diff --git a/src/Client/TwitterClient.cs b/src/Client/TwitterClient.cs index abc6d46..8c92802 100644 --- a/src/Client/TwitterClient.cs +++ b/src/Client/TwitterClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; @@ -129,7 +130,7 @@ private static void InternalIncludesParse(Answer answer) } } - private T[] ParseArrayData(string json) + private Answer ParseArrayData(string json) { var answer = JsonSerializer.Deserialize>(json, _jsonOptions); if (answer.Detail != null || answer.Errors != null) @@ -138,10 +139,11 @@ private T[] ParseArrayData(string json) } if (answer.Data == null) { - return Array.Empty(); + answer.Data = Array.Empty(); + return answer; } InternalIncludesParse(answer); - return answer.Data; + return answer; } private Answer ParseData(string json) @@ -193,7 +195,9 @@ private void BuildRateLimit(HttpResponseHeaders headers, Endpoint endpoint) public async Task GetTweetAsync(string id, TweetSearchOptions options = null) { options ??= new(); - var res = await _httpClient.GetAsync(_baseUrl + "tweets/" + HttpUtility.UrlEncode(id) + "?" + options.Build(true)); + var query = _baseUrl + "tweets/" + HttpUtility.UrlEncode(id) + "?" + options.Build(true); + + var res = await _httpClient.GetAsync(query); BuildRateLimit(res.Headers, Endpoint.GetTweetById); return ParseData(await res.Content.ReadAsStringAsync()).Data; } @@ -205,34 +209,35 @@ public async Task GetTweetAsync(string id, TweetSearchOptions options = n public async Task GetTweetsAsync(string[] ids, TweetSearchOptions options = null) { options ??= new(); + var res = await _httpClient.GetAsync(_baseUrl + "tweets?ids=" + string.Join(",", ids.Select(x => HttpUtility.UrlEncode(x))) + "&" + options.Build(true)); BuildRateLimit(res.Headers, Endpoint.GetTweetsByIds); - return ParseArrayData(await res.Content.ReadAsStringAsync()); + return ParseArrayData(await res.Content.ReadAsStringAsync()).Data; + } /// /// Get the latest tweets of an user /// /// Username of the user you want the tweets of - public async Task GetTweetsFromUserIdAsync(string userId, TweetSearchOptions options = null) + public async Task> GetTweetsFromUserIdAsync(string userId, TweetSearchOptions options = null) { - options ??= new(); - var res = await _httpClient.GetAsync(_baseUrl + "users/" + HttpUtility.HtmlEncode(userId) + "/tweets?" + options.Build(true)); - BuildRateLimit(res.Headers, Endpoint.UserTweetTimeline); - return ParseArrayData(await res.Content.ReadAsStringAsync()); + options ??= new(); + var query = _baseUrl + "users/" + HttpUtility.HtmlEncode(userId) + "/tweets?" + options.Build(true); + return await RequestList(query, Endpoint.UserTweetTimeline); } + /// /// Get the latest tweets for an expression /// /// An expression to build the query /// properties send with the tweet - public async Task GetRecentTweets(Expression expression, TweetSearchOptions options = null) + public async Task> GetRecentTweets(Expression expression, TweetSearchOptions options = null) { options ??= new(); - var res = await _httpClient.GetAsync(_baseUrl + "tweets/search/recent?query=" + HttpUtility.UrlEncode(expression.ToString()) + "&" + options.Build(true)); - BuildRateLimit(res.Headers, Endpoint.RecentSearch); - return ParseArrayData(await res.Content.ReadAsStringAsync()); + var query = _baseUrl + "tweets/search/recent?query=" + HttpUtility.UrlEncode(expression.ToString()) + "&" + options.Build(true); + return await RequestList(query, Endpoint.RecentSearch); } /// @@ -241,12 +246,11 @@ public async Task GetRecentTweets(Expression expression, TweetSearchOpt /// /// An expression to build the query /// properties send with the tweet - public async Task GetAllTweets(Expression expression, TweetSearchOptions options = null) + public async Task> GetAllTweets(Expression expression, TweetSearchOptions options = null) { options ??= new(); - var res = await _httpClient.GetAsync(_baseUrl + "tweets/search/all?query=" + HttpUtility.UrlEncode(expression.ToString()) + "&" + options.Build(true)); - BuildRateLimit(res.Headers, Endpoint.FullArchiveSearch); - return ParseArrayData(await res.Content.ReadAsStringAsync()); + var query = _baseUrl + "tweets/search/all?query=" + HttpUtility.UrlEncode(expression.ToString()) + "&" + options.Build(true); + return await RequestList(query, Endpoint.FullArchiveSearch); } #endregion TweetSearch @@ -257,7 +261,7 @@ public async Task GetInfoTweetStreamAsync() { var res = await _httpClient.GetAsync(_baseUrl + "tweets/search/stream/rules"); BuildRateLimit(res.Headers, Endpoint.ListingFilters); - return ParseArrayData(await res.Content.ReadAsStringAsync()); + return ParseArrayData(await res.Content.ReadAsStringAsync()).Data; } private StreamReader _reader; @@ -346,7 +350,7 @@ public async Task AddTweetStreamAsync(params StreamRequest[] reque var content = new StringContent(JsonSerializer.Serialize(new StreamRequestAdd { Add = request }, _jsonOptions), Encoding.UTF8, "application/json"); var res = await _httpClient.PostAsync(_baseUrl + "tweets/search/stream/rules", content); BuildRateLimit(res.Headers, Endpoint.AddingDeletingFilters); - return ParseArrayData(await res.Content.ReadAsStringAsync()); + return ParseArrayData(await res.Content.ReadAsStringAsync()).Data; } /// @@ -386,7 +390,7 @@ public async Task GetUsersAsync(string[] usernames, UserSearchOptions op options ??= new(); var res = await _httpClient.GetAsync(_baseUrl + $"users/by?usernames={string.Join(",", usernames.Select(x => HttpUtility.UrlEncode(x)))}&{options.Build(false)}"); BuildRateLimit(res.Headers, Endpoint.GetUsersByNames); - return ParseArrayData(await res.Content.ReadAsStringAsync()); + return ParseArrayData(await res.Content.ReadAsStringAsync()).Data; } /// @@ -410,46 +414,24 @@ public async Task GetUsersByIdsAsync(string[] ids, UserSearchOptions opt options ??= new(); var res = await _httpClient.GetAsync(_baseUrl + $"users?ids={string.Join(",", ids.Select(x => HttpUtility.UrlEncode(x)))}&{options.Build(false)}"); BuildRateLimit(res.Headers, Endpoint.GetUsersByIds); - return ParseArrayData(await res.Content.ReadAsStringAsync()); + return ParseArrayData(await res.Content.ReadAsStringAsync()).Data; } #endregion UserSearch #region GetUsers - /// - /// General method for getting the next page of users - /// - /// - private async Task NextUsersAsync(string baseQuery, string token, Endpoint endpoint) - { - var res = await _httpClient.GetAsync(baseQuery + (!baseQuery.EndsWith("?") ? "&" : "") + "pagination_token=" + token); - var data = ParseData(await res.Content.ReadAsStringAsync()); - BuildRateLimit(res.Headers, endpoint); - return new() - { - Users = data.Data, - NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(baseQuery, data.Meta.NextToken, endpoint) - }; - } /// /// Get the follower of an user /// /// ID of the user /// Max number of result, max is 1000 - public async Task GetFollowersAsync(string id, UserSearchOptions options = null) + public async Task> GetFollowersAsync(string id, UserSearchOptions options = null) { options ??= new(); var query = _baseUrl + $"users/{HttpUtility.UrlEncode(id)}/followers?{options.Build(false)}"; - var res = await _httpClient.GetAsync(query); - var data = ParseData(await res.Content.ReadAsStringAsync()); - BuildRateLimit(res.Headers, Endpoint.GetFollowersById); - return new() - { - Users = data.Data, - NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(query, data.Meta.NextToken, Endpoint.GetFollowersById) - }; + return await RequestList(query, Endpoint.GetFollowersById); } /// @@ -457,37 +439,23 @@ public async Task GetFollowersAsync(string id, UserSearchOptions options /// /// ID of the user /// Max number of result, max is 1000 - public async Task GetFollowingAsync(string id, UserSearchOptions options = null) + public async Task> GetFollowingAsync(string id, UserSearchOptions options = null) { options ??= new(); var query = _baseUrl + $"users/{HttpUtility.UrlEncode(id)}/following?{options.Build(false)}"; - var res = await _httpClient.GetAsync(query); - var data = ParseData(await res.Content.ReadAsStringAsync()); - BuildRateLimit(res.Headers, Endpoint.GetFollowingsById); - return new() - { - Users = data.Data, - NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(query, data.Meta.NextToken, Endpoint.GetFollowingsById) - }; + return await RequestList(query, Endpoint.GetFollowersById); } - + /// /// Get the likes of a tweet /// /// ID of the tweet /// This parameter enables you to select which specific user fields will deliver with each returned users objects. You can also set a Limit per page. Max is 100 - public async Task GetLikesAsync(string id, UserSearchOptions options = null) + public async Task> GetLikesAsync(string id, UserSearchOptions options = null) { options ??= new(); var query = _baseUrl + $"tweets/{HttpUtility.UrlEncode(id)}/liking_users?{options.Build(false)}"; - var res = await _httpClient.GetAsync(query); - var data = ParseData(await res.Content.ReadAsStringAsync()); - BuildRateLimit(res.Headers, Endpoint.UsersLiked); - return new() - { - Users = data.Data, - NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(query, data.Meta.NextToken, Endpoint.UsersLiked) - }; + return await RequestList(query, Endpoint.UsersLiked); } /// @@ -495,21 +463,37 @@ public async Task GetLikesAsync(string id, UserSearchOptions options = n /// /// ID of the tweet /// This parameter enables you to select which specific user fields will deliver with each returned users objects. You can also set a Limit per page. Max is 100 - public async Task GetRetweetsAsync(string id, UserSearchOptions options = null) + public async Task> GetRetweetsAsync(string id, UserSearchOptions options = null) { options ??= new(); var query = _baseUrl + $"tweets/{HttpUtility.UrlEncode(id)}/retweeted_by?{options.Build(false)}"; - var res = await _httpClient.GetAsync(query); - var data = ParseData(await res.Content.ReadAsStringAsync()); - BuildRateLimit(res.Headers, Endpoint.RetweetsLookup); + return await RequestList(query, Endpoint.RetweetsLookup); + } + + #endregion Users + + + #region General + + + /// + /// General method for getting the next page with meta token + /// + /// + private async Task> RequestList(string baseQuery, Endpoint endpoint, string token = null) + { + var res = await _httpClient.GetAsync(baseQuery + (string.IsNullOrEmpty(token) ? "" : (!baseQuery.EndsWith("?") ? "&" : "") + "pagination_token=" + token)); + var data = ParseArrayData(await res.Content.ReadAsStringAsync()); + BuildRateLimit(res.Headers, endpoint); return new() { - Users = data.Data, - NextAsync = data.Meta.NextToken == null ? null : async () => await NextUsersAsync(query, data.Meta.NextToken, Endpoint.RetweetsLookup) + Data = data.Data, + NextAsync = data.Meta.NextToken == null ? null : async () => await RequestList(baseQuery, endpoint, data.Meta.NextToken), + PreviousAsync = data.Meta.PreviousToken == null ? null : async () => await RequestList(baseQuery, endpoint, data.Meta.PreviousToken) }; } - #endregion Users + #endregion private const string _baseUrl = "https://api.twitter.com/2/"; diff --git a/src/Response/Answer.cs b/src/Response/Answer.cs index 99e4bb2..6016bfd 100644 --- a/src/Response/Answer.cs +++ b/src/Response/Answer.cs @@ -32,6 +32,7 @@ internal class Meta { public Summary Summary { init; get; } public string NextToken { init; get; } + public string PreviousToken { init; get; } } internal class Summary diff --git a/src/Response/RArray.cs b/src/Response/RArray.cs new file mode 100644 index 0000000..a7cee14 --- /dev/null +++ b/src/Response/RArray.cs @@ -0,0 +1,13 @@ +using System; +using System.Threading.Tasks; + +namespace TwitterSharp.Response +{ + public class RArray + { + public T[] Data { get; set; } + public Func>> NextAsync { init; get; } + public Func>> PreviousAsync { init; get; } + + } +} diff --git a/src/Response/RUser/RUsers.cs b/src/Response/RUser/RUsers.cs deleted file mode 100644 index 1d5b575..0000000 --- a/src/Response/RUser/RUsers.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; -using System.Threading.Tasks; - -namespace TwitterSharp.Response.RUser -{ - public class RUsers - { - public User[] Users { init; get; } - public Func> NextAsync { init; get; } - } -} diff --git a/test/TestFollow.cs b/test/TestFollow.cs index bbf60e1..60fe1e7 100644 --- a/test/TestFollow.cs +++ b/test/TestFollow.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using TwitterSharp.Client; using TwitterSharp.Request.Option; +using TwitterSharp.Response; using TwitterSharp.Response.RUser; namespace TwitterSharp.UnitTests @@ -11,9 +12,9 @@ namespace TwitterSharp.UnitTests [TestClass] public class TestFollow { - private async Task ContainsFollowAsync(string username, RUsers rUsers) + private async Task ContainsFollowAsync(string username, RArray rUsers) { - if (rUsers.Users.Any(x => x.Username == username)) + if (rUsers.Data.Any(x => x.Username == username)) { return true; } diff --git a/test/TestLike.cs b/test/TestLike.cs index 6151ae6..e33d81f 100644 --- a/test/TestLike.cs +++ b/test/TestLike.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using TwitterSharp.Client; using TwitterSharp.Request.Option; +using TwitterSharp.Response; using TwitterSharp.Response.RUser; namespace TwitterSharp.UnitTests @@ -11,9 +12,9 @@ namespace TwitterSharp.UnitTests [TestClass] public class TestLike { - private async Task ContainsLikeAsync(string username, RUsers rUsers) + private async Task ContainsLikeAsync(string username, RArray rUsers) { - if (rUsers.Users.Any(x => x.Username == username)) + if (rUsers.Data.Any(x => x.Username == username)) { return true; } diff --git a/test/TestRetweet.cs b/test/TestRetweet.cs index e9a116d..ced3003 100644 --- a/test/TestRetweet.cs +++ b/test/TestRetweet.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using TwitterSharp.Client; using TwitterSharp.Request.Option; +using TwitterSharp.Response; using TwitterSharp.Response.RUser; namespace TwitterSharp.UnitTests @@ -11,9 +12,9 @@ namespace TwitterSharp.UnitTests [TestClass] public class TestRetweet { - private async Task ContainsUserAsync(string username, RUsers rUsers) + private async Task ContainsUserAsync(string username, RArray rUsers) { - if (rUsers.Users.Any(x => x.Username == username)) + if (rUsers.Data.Any(x => x.Username == username)) { return true; } diff --git a/test/TestTweet.cs b/test/TestTweet.cs index 07a0fe6..26a764e 100644 --- a/test/TestTweet.cs +++ b/test/TestTweet.cs @@ -93,8 +93,8 @@ public async Task GetTweetsFromUserIdAsync() { var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); var answer = await client.GetTweetsFromUserIdAsync("1109748792721432577"); - Assert.AreEqual(10, answer.Length); - Assert.IsNull(answer[0].Author); + Assert.AreEqual(10, answer.Data.Length); + Assert.IsNull(answer.Data[0].Author); } [TestMethod] @@ -105,7 +105,7 @@ public async Task GetTweetsFromUserIdWithSinceIdAsync() { SinceId = "1410551383795781634" }); - Assert.AreEqual(2, answer.Length); + Assert.AreEqual(2, answer.Data.Length); } [TestMethod] @@ -116,7 +116,7 @@ public async Task GetTweetsFromUserIdWithStartTimeAsync() { StartTime = new DateTime(2021, 7, 1, 12, 50, 0) }); - Assert.AreEqual(1, answer.Length); + Assert.AreEqual(1, answer.Data.Length); } [TestMethod] @@ -128,8 +128,8 @@ public async Task GetTweetsFromUserIdWithArgumentsAsync() // Issue #2 TweetOptions = new[] { TweetOption.Attachments }, UserOptions = Array.Empty() }); - Assert.AreEqual(10, answer.Length); - Assert.IsNotNull(answer[0].Author); + Assert.AreEqual(10, answer.Data.Length); + Assert.IsNotNull(answer.Data[0].Author); } [TestMethod] @@ -140,8 +140,8 @@ public async Task GetTweetsFromUserIdWithAuthorAsync() { UserOptions = Array.Empty() }); - Assert.IsTrue(answer.Length == 10); - foreach (var t in answer) + Assert.IsTrue(answer.Data.Length == 10); + foreach (var t in answer.Data) { Assert.IsNotNull(t.Author); Assert.AreEqual("inugamikorone", t.Author.Username); @@ -156,7 +156,25 @@ public async Task GetTweetsFromUserIdWithModifiedLimitAsync() { Limit = 100 }); - Assert.IsTrue(answer.Length == 100); + Assert.IsTrue(answer.Data.Length == 100); + } + + [TestMethod] + public async Task GetTweetsFromUserIdWithNextTokenAndPreviousToken() + { + var client = new TwitterClient(Environment.GetEnvironmentVariable("TWITTER_TOKEN")); + var answer = await client.GetTweetsFromUserIdAsync("1109748792721432577"); + + Assert.IsTrue(answer.Data.Length == 10); + + var nextAnswer = await answer.NextAsync(); + + Assert.IsTrue(nextAnswer.Data.Length == 10); + + var previousAnswer = await nextAnswer.PreviousAsync(); + + Assert.IsTrue(previousAnswer.Data.Length == 10); + Assert.IsTrue(previousAnswer.Data[0].Id.Equals(answer.Data[0].Id)); } [TestMethod] @@ -449,7 +467,7 @@ public async Task GetRecentTweets() // note: retweets are truncated at 140 characters, so i had to exclude them for making the check reliable var a = await client.GetRecentTweets(Expression.Hashtag(hashtag).And(Expression.IsRetweet().Negate())); - Assert.IsTrue(a.All(x => x.Text.Contains("#"+hashtag, StringComparison.InvariantCultureIgnoreCase))); + Assert.IsTrue(a.Data.All(x => x.Text.Contains("#"+hashtag, StringComparison.InvariantCultureIgnoreCase))); } [TestMethod]