Build a Twitter bot using .NET Core in under 12 million lines of code

Walkthrough

I came across a link on Reddit today to a post about building a Twitter bot in Node.js in 38 lines of code. Some commenters remarked about how 38 lines is disingenuous. It got me thinking about doing the same in a .NET Core console application, and how many lines of code is, really?

As far as I can tell, when running a simple .NET Core console app, you're using the CoreCLR and the CoreFX libraries. CoreCLR contains the runtime and the minimum types necessary. This project contains just over 9 million lines, mostly in IL, C#, and C++ files. CoreFX rounds out the standard types, and contains approximately 2.5 million lines of code. Essentially all of that is in C#, with the next most frequent being MSBuild scripts. That brings us to right around 11.5 million lines of code. The library we'll be using is callled CoreTweet, and contains around 8 thousand lines of C# code. Finally, to accomplish our goal we'll need just 34 lines of code, making us come in under our figure of 12 million lines.

All of these figures we retrieved by pulling the repositories and running CLOC on them.

If you want to follow along, I'll be using the following tools, so make sure they're installed.

Setting up the console project

Open up VS Code, head to the terminal, ( Ctrl+` ) and create a new folder for your project in your desired location. Navigate into your new folder, and run dotnet new console . Once that completes, open the newly created folder with the project inside of VS Code. Go back to your Powershell terminal and run the dotnet add package CoreTweet command. It might prompt you to install Nuget, then promp to trust nuget.org as a source. Confirm both, and once all packages are installed, run a dotnet restore to ensure all packages have been pulled down. After all of that, you should be able to execute the dotnet run command, and see the Hello World! message in the terminal.

Async In a Console App

The first item of concern is that the CoreTweet API operates via async methods, which are not easily awaited in a console application. So we'll make a new static async method below main to put all of our async calls inside of.

static async Task MainAsync() { // Async method calls can be easiily awaited here }

This can be called from our main method with Wait to run the method to completion. Replace the Hello World message with a call to MainAsync like below.

static void Main(string[] args) { MainAsync().Wait(); Console.ReadLine(); }

I also added a ReadLine to keep the window open.

Twitter Interaction

Now, we'll add the Twitter interaction. It all starts with generating your keys for the Twitter API, here. Go ahead and reference the article or google if you need help. You'll need two two consumer keys and access keys. Once keys are generated, we create a 'Token.' This token is just an instance of all the necessary authentication bits that can be used to reach out to the Twitter API successfully. I'm passing my keys in via command-line parameters, which come in as an array of strings. You can do the same, use a config file, or use environment variables. Just don't commit secrets to your version control.

var twitter = Tokens.Create(args[0], args[1], args[2], args[3]);

From here, we can use this twitter object for easy API interaction. First we'll search for tweets with the #dotnetcore hashtag.

var tweets = await twitter.Search.TweetsAsync(q => "#dotnetcore", lang => "en", count => 10, result_type => "recent");

This creates a search request with a query (q), and some options to ensure that I only get 10 Tweets that I care about.

The tweets variable will store the SearchResult after execution, and we can iterate over that SearchResult. I'm going to mirror what the Node.js article did, and favorite each tweet.

foreach(var tweet in tweets) { try { var resultantTweet = await twitter.Favorites.CreateAsync(id => tweet.Id); Console.WriteLine($"Favorited tweet: 'https://twitter.com/{resultantTweet.User.ScreenName}/status/{resultantTweet.Id}'"); } catch { Console.WriteLine($"Couldn't favorite: 'https://twitter.com/{tweet.User.ScreenName}/status/{tweet.Id}'"); } }

Here, I am looping over each tweet, then Creating a Favorite from the tweet's Id field. There's some error handling and console logging to let you know what's happening. At this point you should be able to dotnet run, and have it favoriting tweets. If not, feel free to reach out. See the full code below.

using System; using CoreTweet; using System.Threading.Tasks; namespace TwitterDemo { class Program { static void Main(string[] args) { MainAsync(args).Wait(); Console.ReadLine(); } static async Task MainAsync(string[] args) { var twitter = Tokens.Create(args[0], args[1], args[2], args[3]); var tweets = await twitter.Search.TweetsAsync(q => "#dotnetcore", lang => "en", count => 10, result_type => "recent"); foreach(var tweet in tweets) { try { var resultantTweet = await twitter.Favorites.CreateAsync(id => tweet.Id); Console.WriteLine($"Favorited tweet: 'https://twitter.com/{resultantTweet.User.ScreenName}/status/{resultantTweet.Id}'"); } catch { Console.WriteLine($"Couldn't favorite: 'https://twitter.com/{tweet.User.ScreenName}/status/{tweet.Id}'"); } } } } }

My next post will be about taking what we've done here, and actually make it a bot. My intention is to run the code on Azure Functions and/or AWS Lambda.

Addendum: Improvements and Tweaks

Reddit user /u/SagdiyevOfficial mentioned that more than one core can be utilized by running the foreach iterations in parallel. If you want to try that, check out the code below, and the examplle for Parallel.ForEach on MSDN.

Make sure that you have the async keyword in the beginning of your lambda, it's necessary to be able to await calls in the body.

Parallel.ForEach(tweets, async tweet => { try { var resultantTweet = await twitter.Favorites.CreateAsync(id => tweet.Id); Console.WriteLine($"Favorited tweet: 'https://twitter.com/{resultantTweet.User.ScreenName}/status/{resultantTweet.Id}'"); } catch { Console.WriteLine($"Couldn't favorite: 'https://twitter.com/{tweet.User.ScreenName}/status/{tweet.Id}'"); } });

This snippet is a drop-in replacement for the existing foreach loop.