diff --git a/Samples/LinqToTwitter6/Console/ConsoleDemo.CSharp/ConsoleDemo.CSharp/UserDemos.cs b/Samples/LinqToTwitter6/Console/ConsoleDemo.CSharp/ConsoleDemo.CSharp/UserDemos.cs index 2bfdc0f0..7c64cc8d 100644 --- a/Samples/LinqToTwitter6/Console/ConsoleDemo.CSharp/ConsoleDemo.CSharp/UserDemos.cs +++ b/Samples/LinqToTwitter6/Console/ConsoleDemo.CSharp/ConsoleDemo.CSharp/UserDemos.cs @@ -49,6 +49,14 @@ internal static async Task RunAsync(TwitterContext twitterCtx) Console.WriteLine("\n\tReport spammer...\n"); await ReportSpammerAsync(twitterCtx); break; + case '7': + Console.WriteLine("\n\tFinding followers...\n"); + await FindFollowersAsync(twitterCtx); + break; + case '8': + Console.WriteLine("\n\tFinding following...\n"); + await FindFollowingAsync(twitterCtx); + break; case 'q': case 'Q': Console.WriteLine("\nReturning...\n"); @@ -72,6 +80,8 @@ static void ShowMenu() Console.WriteLine("\t 4. Account Contributors"); Console.WriteLine("\t 5. Get Profile Banner Sizes"); Console.WriteLine("\t 6. Report Spammer"); + Console.WriteLine("\t 7. Find Followers"); + Console.WriteLine("\t 8. Find Following"); Console.WriteLine(); Console.Write("\t Q. Return to Main menu"); } @@ -197,5 +207,39 @@ static async Task ReportSpammerAsync(TwitterContext twitterCtx) Console.WriteLine("You just reported {0} as a spammer.", spammer?.ScreenNameResponse); } + + async static Task FindFollowersAsync(TwitterContext twitterCtx) + { + string userID = "15411837"; + + TwitterUserQuery? userResponse = + await + (from user in twitterCtx.TwitterUser + where user.Type == UserType.Followers && + user.ID == userID + select user) + .SingleOrDefaultAsync(); + + if (userResponse != null) + userResponse.Users?.ForEach(user => + Console.WriteLine("Name: " + user.Username)); + } + + async static Task FindFollowingAsync(TwitterContext twitterCtx) + { + string userID = "15411837"; + + TwitterUserQuery? userResponse = + await + (from user in twitterCtx.TwitterUser + where user.Type == UserType.Following && + user.ID == userID + select user) + .SingleOrDefaultAsync(); + + if (userResponse != null) + userResponse.Users?.ForEach(user => + Console.WriteLine("ID: " + user.ID)); + } } } diff --git a/src/LinqToTwitter6/LinqToTwitter.AspNet/LinqToTwitter.AspNet.csproj b/src/LinqToTwitter6/LinqToTwitter.AspNet/LinqToTwitter.AspNet.csproj index 4c1d49b2..ad9c7f2c 100644 --- a/src/LinqToTwitter6/LinqToTwitter.AspNet/LinqToTwitter.AspNet.csproj +++ b/src/LinqToTwitter6/LinqToTwitter.AspNet/LinqToTwitter.AspNet.csproj @@ -2,7 +2,7 @@ net5.0 - 6.0.1 + 6.2.0 Joe Mayo Joe Mayo LINQ to Twitter for ASP.NET diff --git a/src/LinqToTwitter6/LinqToTwitter.Tests/UserTests/TwitterUserRequestProcessorTests.cs b/src/LinqToTwitter6/LinqToTwitter.Tests/UserTests/TwitterUserRequestProcessorTests.cs index 30762ba0..0e22a5d2 100644 --- a/src/LinqToTwitter6/LinqToTwitter.Tests/UserTests/TwitterUserRequestProcessorTests.cs +++ b/src/LinqToTwitter6/LinqToTwitter.Tests/UserTests/TwitterUserRequestProcessorTests.cs @@ -29,9 +29,12 @@ public void GetParametersTest() Expression> expression = tweet => tweet.Type == UserType.IdLookup && + tweet.ID == "456" && tweet.Ids == "2,3" && tweet.Usernames == "joemayo,linq2twitr" && - tweet.Expansions == "attachments.poll_ids,author_id" && + tweet.MaxResults == 50 && + tweet.PaginationToken == "123" && + tweet.Expansions == "attachments.poll_ids,author_id" && tweet.TweetFields == "author_id,created_at" && tweet.UserFields == "created_at,verified"; @@ -42,7 +45,10 @@ public void GetParametersTest() Assert.IsTrue( queryParams.Contains( new KeyValuePair(nameof(TwitterUserQuery.Type), ((int)UserType.IdLookup).ToString(CultureInfo.InvariantCulture)))); - Assert.IsTrue( + Assert.IsTrue( + queryParams.Contains( + new KeyValuePair(nameof(TwitterUserQuery.ID), "456"))); + Assert.IsTrue( queryParams.Contains( new KeyValuePair(nameof(TwitterUserQuery.Ids), "2,3"))); Assert.IsTrue( @@ -51,7 +57,13 @@ public void GetParametersTest() Assert.IsTrue( queryParams.Contains( new KeyValuePair(nameof(TwitterUserQuery.Expansions), "attachments.poll_ids,author_id"))); - Assert.IsTrue( + Assert.IsTrue( + queryParams.Contains( + new KeyValuePair(nameof(TwitterUserQuery.MaxResults), "50"))); + Assert.IsTrue( + queryParams.Contains( + new KeyValuePair(nameof(TwitterUserQuery.PaginationToken), "123"))); + Assert.IsTrue( queryParams.Contains( new KeyValuePair(nameof(TwitterUserQuery.TweetFields), "author_id,created_at"))); Assert.IsTrue( @@ -60,7 +72,7 @@ public void GetParametersTest() } [TestMethod] - public void BuildUrl_WithIds_IncludesParameters() + public void BuildUrl_ForIdLookup_IncludesParameters() { const string ExpectedUrl = BaseUrl2 + "users?" + @@ -84,6 +96,62 @@ public void BuildUrl_WithIds_IncludesParameters() Assert.AreEqual(ExpectedUrl, req.FullUrl); } + [TestMethod] + public void BuildUrl_ForFollowing_IncludesParameters() + { + const string ExpectedUrl = + BaseUrl2 + "users/123/following?" + + "max_results=50&" + + "pagination_token=456&" + + "expansions=attachments.poll_ids%2Cauthor_id&" + + "tweet.fields=author_id%2Ccreated_at&" + + "user.fields=created_at%2Cverified"; + var twitterUserReqProc = new TwitterUserRequestProcessor { BaseUrl = BaseUrl2 }; + var parameters = + new Dictionary + { + { nameof(TwitterUserQuery.Type), UserType.Following.ToString() }, + { nameof(TwitterUserQuery.ID), "123" }, + { nameof(TwitterUserQuery.Expansions), "attachments.poll_ids,author_id" }, + { nameof(TwitterUserQuery.MaxResults), "50" }, + { nameof(TwitterUserQuery.PaginationToken), "456" }, + { nameof(TwitterUserQuery.TweetFields), "author_id,created_at" }, + { nameof(TwitterUserQuery.UserFields), "created_at,verified" }, + }; + + Request req = twitterUserReqProc.BuildUrl(parameters); + + Assert.AreEqual(ExpectedUrl, req.FullUrl); + } + + [TestMethod] + public void BuildUrl_ForFollowers_IncludesParameters() + { + const string ExpectedUrl = + BaseUrl2 + "users/123/followers?" + + "max_results=50&" + + "pagination_token=456&" + + "expansions=attachments.poll_ids%2Cauthor_id&" + + "tweet.fields=author_id%2Ccreated_at&" + + "user.fields=created_at%2Cverified"; + var twitterUserReqProc = new TwitterUserRequestProcessor { BaseUrl = BaseUrl2 }; + var parameters = + new Dictionary + { + { nameof(TwitterUserQuery.Type), UserType.Followers.ToString() }, + { nameof(TwitterUserQuery.ID), "123" }, + { nameof(TwitterUserQuery.Expansions), "attachments.poll_ids,author_id" }, + { nameof(TwitterUserQuery.MaxResults), "50" }, + { nameof(TwitterUserQuery.PaginationToken), "456" }, + { nameof(TwitterUserQuery.TweetFields), "author_id,created_at" }, + { nameof(TwitterUserQuery.UserFields), "created_at,verified" }, + }; + + Request req = twitterUserReqProc.BuildUrl(parameters); + + Assert.AreEqual(ExpectedUrl, req.FullUrl); + } + [TestMethod] public void BuildUrl_WithUsernames_IncludesParameters() { @@ -163,6 +231,42 @@ public void BuildUrl_WithoutIdsOnIdLookup_Throws() Assert.AreEqual(nameof(TwitterUserQuery.Ids), ex.ParamName); } + [TestMethod] + public void BuildUrl_WithoutIDOnFollowers_Throws() + { + var twitterUserReqProc = new TwitterUserRequestProcessor { BaseUrl = BaseUrl2 }; + var parameters = + new Dictionary + { + { nameof(TwitterUserQuery.Type), UserType.Followers.ToString() }, + //{ nameof(TwitterUserQuery.ID), null } + }; + + ArgumentException ex = + L2TAssert.Throws(() => + twitterUserReqProc.BuildUrl(parameters)); + + Assert.AreEqual(nameof(TwitterUserQuery.ID), ex.ParamName); + } + + [TestMethod] + public void BuildUrl_WithoutIDOnFollowing_Throws() + { + var twitterUserReqProc = new TwitterUserRequestProcessor { BaseUrl = BaseUrl2 }; + var parameters = + new Dictionary + { + { nameof(TwitterUserQuery.Type), UserType.Following.ToString() }, + //{ nameof(TwitterUserQuery.ID), null } + }; + + ArgumentException ex = + L2TAssert.Throws(() => + twitterUserReqProc.BuildUrl(parameters)); + + Assert.AreEqual(nameof(TwitterUserQuery.ID), ex.ParamName); + } + [TestMethod] public void BuildUrl_WithoutUsernamesOnUsernameLookup_Throws() { @@ -288,9 +392,12 @@ public void ProcessResults_Populates_Input_Parameters() { BaseUrl = BaseUrl2, Type = UserType.IdLookup, + ID = "890", Ids = "3,7", Usernames = "9,0", Expansions = "123", + MaxResults = 50, + PaginationToken = "567", TweetFields = "678", UserFields = "234" }; @@ -302,9 +409,12 @@ public void ProcessResults_Populates_Input_Parameters() var twitterUserQuery = results.Single(); Assert.IsNotNull(twitterUserQuery); Assert.AreEqual(UserType.IdLookup, twitterUserQuery.Type); + Assert.AreEqual("890", twitterUserQuery.ID); Assert.AreEqual("3,7", twitterUserQuery.Ids); Assert.AreEqual("9,0", twitterUserQuery.Usernames); Assert.AreEqual("123", twitterUserQuery.Expansions); + Assert.AreEqual(50, twitterUserQuery.MaxResults); + Assert.AreEqual("567", twitterUserQuery.PaginationToken); Assert.AreEqual("678", twitterUserQuery.TweetFields); Assert.AreEqual("234", twitterUserQuery.UserFields); } diff --git a/src/LinqToTwitter6/LinqToTwitter/LinqToTwitter.csproj b/src/LinqToTwitter6/LinqToTwitter/LinqToTwitter.csproj index a922b5c2..2d192026 100644 --- a/src/LinqToTwitter6/LinqToTwitter/LinqToTwitter.csproj +++ b/src/LinqToTwitter6/LinqToTwitter/LinqToTwitter.csproj @@ -4,7 +4,7 @@ net5.0 enable LINQ to Twitter is a 3rd party LINQ Provider that lets you tweet and query with the Twitter API. - 6.1.0 + 6.2.0 Joe Mayo Joe Mayo linqtotwitter diff --git a/src/LinqToTwitter6/LinqToTwitter/User/TwitterUserQuery.cs b/src/LinqToTwitter6/LinqToTwitter/User/TwitterUserQuery.cs index 7d0ece3c..f7e1e0fc 100644 --- a/src/LinqToTwitter6/LinqToTwitter/User/TwitterUserQuery.cs +++ b/src/LinqToTwitter6/LinqToTwitter/User/TwitterUserQuery.cs @@ -32,6 +32,21 @@ public record TwitterUserQuery /// public string? Expansions { get; init; } + /// + /// User ID for following/follower queries + /// + public string? ID { get; init; } + + /// + /// Max number of tweets to return per requrest - default 100 - possible 1000 + /// + public int MaxResults { get; init; } + + /// + /// If set, with token from previous response metadata, pages forward or backward + /// + public string? PaginationToken { get; init; } + /// /// Comma-separated list of fields to return in the Tweet object /// diff --git a/src/LinqToTwitter6/LinqToTwitter/User/TwitterUserRequestProcessor.cs b/src/LinqToTwitter6/LinqToTwitter/User/TwitterUserRequestProcessor.cs index 748b54e3..b727edf2 100644 --- a/src/LinqToTwitter6/LinqToTwitter/User/TwitterUserRequestProcessor.cs +++ b/src/LinqToTwitter6/LinqToTwitter/User/TwitterUserRequestProcessor.cs @@ -22,6 +22,11 @@ public class TwitterUserRequestProcessor : IRequestProcessor, IRequestProc /// public UserType Type { get; set; } + /// + /// User ID for following/follower queries + /// + public string? ID { get; set; } + /// /// Required for ID queries - Up to 100 comma-separated IDs to search for /// @@ -37,6 +42,16 @@ public class TwitterUserRequestProcessor : IRequestProcessor, IRequestProc /// public string? Expansions { get; set; } + /// + /// Max number of tweets to return per requrest - default 100 - possible 1000 + /// + public int MaxResults { get; set; } + + /// + /// If set, with token from previous response metadata, pages forward or backward + /// + public string? PaginationToken { get; set; } + /// /// Comma-separated list of fields to return in the Tweet object - /// @@ -59,9 +74,12 @@ public Dictionary GetParameters(LambdaExpression lambdaExpressio lambdaExpression.Body, new List { nameof(Type), + nameof(ID), nameof(Ids), nameof(Usernames), nameof(Expansions), + nameof(MaxResults), + nameof(PaginationToken), nameof(TweetFields), nameof(UserFields) }) ; @@ -85,6 +103,8 @@ public Request BuildUrl(Dictionary parameters) { UserType.IdLookup => BuildIdLookupUrl(parameters), UserType.UsernameLookup => BuildUsernameLookupUrl(parameters), + UserType.Followers => BuildFollowersUrl(parameters), + UserType.Following => BuildFollowingUrl(parameters), _ => throw new InvalidOperationException("The default case of BuildUrl should never execute because a Type must be specified."), }; } @@ -139,6 +159,74 @@ Request BuildUsernameLookupUrl(Dictionary parameters) return req; } + /// + /// builds a url for people following user + /// + /// url parameters + /// new url for request + Request BuildFollowersUrl(Dictionary parameters) + { + SetUserID(parameters); + + var req = new Request($"{BaseUrl}users/{ID}/followers"); + + BuildFollowParameters(parameters, req); + + return req; + } + + /// + /// builds a url for people user follows + /// + /// url parameters + /// new url for request + Request BuildFollowingUrl(Dictionary parameters) + { + SetUserID(parameters); + + var req = new Request($"{BaseUrl}users/{ID}/following"); + + BuildFollowParameters(parameters, req); + + return req; + } + + /// + /// builds parameters common to timeline queries + /// + /// parameters to process + /// object + void BuildFollowParameters(Dictionary parameters, Request req) + { + var urlParams = req.RequestParameters; + + if (parameters.ContainsKey(nameof(MaxResults))) + { + MaxResults = int.Parse(parameters[nameof(MaxResults)]); + urlParams.Add(new QueryParameter("max_results", MaxResults.ToString())); + } + + if (parameters.ContainsKey(nameof(PaginationToken))) + { + PaginationToken = parameters[nameof(PaginationToken)]; + urlParams.Add(new QueryParameter("pagination_token", PaginationToken)); + } + + BuildSharedUrlParameters(urlParams, parameters); + } + + /// + /// Used by follower/following queries - sets parameter, but doesn't treat as a query parameter. + /// + /// list of parameters + void SetUserID(Dictionary parameters) + { + if (parameters.ContainsKey(nameof(ID))) + ID = parameters[nameof(ID)]; + else + throw new ArgumentException($"{nameof(ID)} is required", nameof(ID)); + } + /// /// Appends parameters for User requests /// @@ -197,9 +285,12 @@ TwitterUserQuery JsonDeserialize(string responseJson) return new TwitterUserQuery { Type = Type, + ID = ID, Ids = Ids, Usernames = Usernames, Expansions = Expansions, + MaxResults = MaxResults, + PaginationToken = PaginationToken, TweetFields = TweetFields, UserFields = UserFields }; @@ -207,9 +298,12 @@ TwitterUserQuery JsonDeserialize(string responseJson) return user with { Type = Type, + ID = ID, Ids = Ids, Usernames = Usernames, Expansions = Expansions, + MaxResults = MaxResults, + PaginationToken = PaginationToken, TweetFields = TweetFields, UserFields = UserFields }; diff --git a/src/LinqToTwitter6/LinqToTwitter/User/UserType.cs b/src/LinqToTwitter6/LinqToTwitter/User/UserType.cs index 30f9fe89..49b490b5 100644 --- a/src/LinqToTwitter6/LinqToTwitter/User/UserType.cs +++ b/src/LinqToTwitter6/LinqToTwitter/User/UserType.cs @@ -34,5 +34,15 @@ public enum UserType /// Search users by username /// UsernameLookup, + + /// + /// Get list of people that are following user + /// + Followers, + + /// + /// Get list of people that user is following + /// + Following, } }