Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 56 additions & 72 deletions src/Client/TwitterClient.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
Expand Down Expand Up @@ -129,7 +130,7 @@ private static void InternalIncludesParse<T>(Answer<T[]> answer)
}
}

private T[] ParseArrayData<T>(string json)
private Answer<T[]> ParseArrayData<T>(string json)
{
var answer = JsonSerializer.Deserialize<Answer<T[]>>(json, _jsonOptions);
if (answer.Detail != null || answer.Errors != null)
Expand All @@ -138,10 +139,11 @@ private T[] ParseArrayData<T>(string json)
}
if (answer.Data == null)
{
return Array.Empty<T>();
answer.Data = Array.Empty<T>();
return answer;
}
InternalIncludesParse(answer);
return answer.Data;
return answer;
}

private Answer<T> ParseData<T>(string json)
Expand Down Expand Up @@ -193,7 +195,9 @@ private void BuildRateLimit(HttpResponseHeaders headers, Endpoint endpoint)
public async Task<Tweet> 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<Tweet>(await res.Content.ReadAsStringAsync()).Data;
}
Expand All @@ -205,34 +209,35 @@ public async Task<Tweet> GetTweetAsync(string id, TweetSearchOptions options = n
public async Task<Tweet[]> 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<Tweet>(await res.Content.ReadAsStringAsync());
return ParseArrayData<Tweet>(await res.Content.ReadAsStringAsync()).Data;

}

/// <summary>
/// Get the latest tweets of an user
/// </summary>
/// <param name="userId">Username of the user you want the tweets of</param>
public async Task<Tweet[]> GetTweetsFromUserIdAsync(string userId, TweetSearchOptions options = null)
public async Task<RArray<Tweet>> 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<Tweet>(await res.Content.ReadAsStringAsync());
options ??= new();
var query = _baseUrl + "users/" + HttpUtility.HtmlEncode(userId) + "/tweets?" + options.Build(true);
return await RequestList<Tweet>(query, Endpoint.UserTweetTimeline);
}


/// <summary>
/// Get the latest tweets for an expression
/// </summary>
/// <param name="expression">An expression to build the query <seealso cref="https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/build-a-query"/></param>
/// <param name="options">properties send with the tweet</param>
public async Task<Tweet[]> GetRecentTweets(Expression expression, TweetSearchOptions options = null)
public async Task<RArray<Tweet>> 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<Tweet>(await res.Content.ReadAsStringAsync());
var query = _baseUrl + "tweets/search/recent?query=" + HttpUtility.UrlEncode(expression.ToString()) + "&" + options.Build(true);
return await RequestList<Tweet>(query, Endpoint.RecentSearch);
}

/// <summary>
Expand All @@ -241,12 +246,11 @@ public async Task<Tweet[]> GetRecentTweets(Expression expression, TweetSearchOpt
/// </summary>
/// <param name="expression">An expression to build the query <seealso cref="https://developer.twitter.com/en/docs/twitter-api/tweets/search/integrate/build-a-query"/></param>
/// <param name="options">properties send with the tweet</param>
public async Task<Tweet[]> GetAllTweets(Expression expression, TweetSearchOptions options = null)
public async Task<RArray<Tweet>> 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<Tweet>(await res.Content.ReadAsStringAsync());
var query = _baseUrl + "tweets/search/all?query=" + HttpUtility.UrlEncode(expression.ToString()) + "&" + options.Build(true);
return await RequestList<Tweet>(query, Endpoint.FullArchiveSearch);
}

#endregion TweetSearch
Expand All @@ -257,7 +261,7 @@ public async Task<StreamInfo[]> GetInfoTweetStreamAsync()
{
var res = await _httpClient.GetAsync(_baseUrl + "tweets/search/stream/rules");
BuildRateLimit(res.Headers, Endpoint.ListingFilters);
return ParseArrayData<StreamInfo>(await res.Content.ReadAsStringAsync());
return ParseArrayData<StreamInfo>(await res.Content.ReadAsStringAsync()).Data;
}

private StreamReader _reader;
Expand Down Expand Up @@ -346,7 +350,7 @@ public async Task<StreamInfo[]> 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<StreamInfo>(await res.Content.ReadAsStringAsync());
return ParseArrayData<StreamInfo>(await res.Content.ReadAsStringAsync()).Data;
}

/// <summary>
Expand Down Expand Up @@ -386,7 +390,7 @@ public async Task<User[]> 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<User>(await res.Content.ReadAsStringAsync());
return ParseArrayData<User>(await res.Content.ReadAsStringAsync()).Data;
}

/// <summary>
Expand All @@ -410,106 +414,86 @@ public async Task<User[]> 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<User>(await res.Content.ReadAsStringAsync());
return ParseArrayData<User>(await res.Content.ReadAsStringAsync()).Data;
}

#endregion UserSearch

#region GetUsers

/// <summary>
/// General method for getting the next page of users
/// </summary>
/// <returns></returns>
private async Task<RUsers> NextUsersAsync(string baseQuery, string token, Endpoint endpoint)
{
var res = await _httpClient.GetAsync(baseQuery + (!baseQuery.EndsWith("?") ? "&" : "") + "pagination_token=" + token);
var data = ParseData<User[]>(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)
};
}

/// <summary>
/// Get the follower of an user
/// </summary>
/// <param name="id">ID of the user</param>
/// <param name="limit">Max number of result, max is 1000</param>
public async Task<RUsers> GetFollowersAsync(string id, UserSearchOptions options = null)
public async Task<RArray<User>> 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<User[]>(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<User>(query, Endpoint.GetFollowersById);
}

/// <summary>
/// Get the following of an user
/// </summary>
/// <param name="id">ID of the user</param>
/// <param name="limit">Max number of result, max is 1000</param>
public async Task<RUsers> GetFollowingAsync(string id, UserSearchOptions options = null)
public async Task<RArray<User>> 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<User[]>(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<User>(query, Endpoint.GetFollowersById);
}

/// <summary>
/// Get the likes of a tweet
/// </summary>
/// <param name="id">ID of the tweet</param>
/// <param name="options">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</param>
public async Task<RUsers> GetLikesAsync(string id, UserSearchOptions options = null)
public async Task<RArray<User>> 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<User[]>(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<User>(query, Endpoint.UsersLiked);
}

/// <summary>
/// Get the retweets of a tweet
/// </summary>
/// <param name="id">ID of the tweet</param>
/// <param name="options">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</param>
public async Task<RUsers> GetRetweetsAsync(string id, UserSearchOptions options = null)
public async Task<RArray<User>> 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<User[]>(await res.Content.ReadAsStringAsync());
BuildRateLimit(res.Headers, Endpoint.RetweetsLookup);
return await RequestList<User>(query, Endpoint.RetweetsLookup);
}

#endregion Users


#region General


/// <summary>
/// General method for getting the next page with meta token
/// </summary>
/// <returns></returns>
private async Task<RArray<T>> RequestList<T>(string baseQuery, Endpoint endpoint, string token = null)
{
var res = await _httpClient.GetAsync(baseQuery + (string.IsNullOrEmpty(token) ? "" : (!baseQuery.EndsWith("?") ? "&" : "") + "pagination_token=" + token));
var data = ParseArrayData<T>(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<T>(baseQuery, endpoint, data.Meta.NextToken),
PreviousAsync = data.Meta.PreviousToken == null ? null : async () => await RequestList<T>(baseQuery, endpoint, data.Meta.PreviousToken)
};
}

#endregion Users
#endregion

private const string _baseUrl = "https://api.twitter.com/2/";

Expand Down
1 change: 1 addition & 0 deletions src/Response/Answer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/Response/RArray.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System;
using System.Threading.Tasks;

namespace TwitterSharp.Response
{
public class RArray<T>
{
public T[] Data { get; set; }
public Func<Task<RArray<T>>> NextAsync { init; get; }
public Func<Task<RArray<T>>> PreviousAsync { init; get; }

}
}
11 changes: 0 additions & 11 deletions src/Response/RUser/RUsers.cs

This file was deleted.

5 changes: 3 additions & 2 deletions test/TestFollow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
using System.Threading.Tasks;
using TwitterSharp.Client;
using TwitterSharp.Request.Option;
using TwitterSharp.Response;
using TwitterSharp.Response.RUser;

namespace TwitterSharp.UnitTests
{
[TestClass]
public class TestFollow
{
private async Task<bool> ContainsFollowAsync(string username, RUsers rUsers)
private async Task<bool> ContainsFollowAsync(string username, RArray<User> rUsers)
{
if (rUsers.Users.Any(x => x.Username == username))
if (rUsers.Data.Any(x => x.Username == username))
{
return true;
}
Expand Down
5 changes: 3 additions & 2 deletions test/TestLike.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
using System.Threading.Tasks;
using TwitterSharp.Client;
using TwitterSharp.Request.Option;
using TwitterSharp.Response;
using TwitterSharp.Response.RUser;

namespace TwitterSharp.UnitTests
{
[TestClass]
public class TestLike
{
private async Task<bool> ContainsLikeAsync(string username, RUsers rUsers)
private async Task<bool> ContainsLikeAsync(string username, RArray<User> rUsers)
{
if (rUsers.Users.Any(x => x.Username == username))
if (rUsers.Data.Any(x => x.Username == username))
{
return true;
}
Expand Down
5 changes: 3 additions & 2 deletions test/TestRetweet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,17 @@
using System.Threading.Tasks;
using TwitterSharp.Client;
using TwitterSharp.Request.Option;
using TwitterSharp.Response;
using TwitterSharp.Response.RUser;

namespace TwitterSharp.UnitTests
{
[TestClass]
public class TestRetweet
{
private async Task<bool> ContainsUserAsync(string username, RUsers rUsers)
private async Task<bool> ContainsUserAsync(string username, RArray<User> rUsers)
{
if (rUsers.Users.Any(x => x.Username == username))
if (rUsers.Data.Any(x => x.Username == username))
{
return true;
}
Expand Down
Loading