This blog definitely wasn’t supposed to be about this, but I got side-tracked and kept on coding, so I let it happen.
Today — whilst I should have been working on a different project — I got a little bit carried away messing around with the Twitter api. As I was trying different requests using their handy postman collection I came up with a funny twitter bot idea. One that might cause a bit of a sh*t storm on the site, so I coded it.
The idea is a bot that tracks the celebrities that other celebrities are following. If — for some reason or another — one of these celebrities decides to perform an unfollow on the other. My handy bot will do the honour of @ing both the victim and the perpetrator to inform them of the occurrence.
Meet Spicy Unfollows, note — at the time of writing this — the only unfollow it’s detected is of me unfollowing for testing purposes:

Followed swiftly with a reply 😂:

How does it work?
I created an Azure Timer Trigger to execute every 16 minutes.
[FunctionName("FindSpicyUnfollows")]
public async Task RunAsync([TimerTrigger("0 */16 * * * *")] TimerInfo myTimer, ILogger log)
{
For the endpoints I’m using Twitter throttles requests in 15 minute chunks, hence the 16 minute delay.
I then fetch the celebrities I’m tracking from table storage, I simply add their username and name then the bot figures out the rest:
public class Celeb
{
[RowKey]
public string Id { get; set; }
public string Name { get; set; }
public string Username { get; set; }
public string TwitterId { get; set; }
public IEnumerable<string> Following { get; set; }
}
Given you’ve got the username
of a Twitter account, you can make use of the User by Username
endpoint:
GET: https://api.twitter.com/2/users/by/username/:username
public async Task <TwitterUser> GetUserByUsername(string username) {
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("TWITTER_BEARER"));
var response = await client.GetAsync($"https://api.twitter.com/2/users/by/username/{username}");
var data = await response.Content.ReadAsStringAsync();
var twitterUser = JsonConvert.DeserializeObject<TwitterUser> (data);
return twitterUser;
}
This gives you the following response:
using Newtonsoft.Json;
namespace Codeheir.Spicy.TwitterSdk
{
public class Data
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("username")]
public string Username { get; set; }
}
public class TwitterUser
{
[JsonProperty("data")]
public Data Data { get; set; }
}
}
Which provides us with the id, so we’re good to go.
Tip: make use of https://json2csharp.com/ to convert JSON to C# models, whenever I’m consuming an API my approach tends to be the following:
- Make the request in Postman
- Copy the response of the request
- Paste the response into json2csharp
- Enable the setting “Use Pascal Case”
- Copy the model into code
Next, now we’ve got the ids for all the celebs. If it’s the first time running the bot for a given celebrity then there’s nothing to compare against, so just persist the data.
var celebs = await celebRepo.GetAll("1");
foreach(var celeb in celebs) {
if (string.IsNullOrEmpty(celeb.TwitterId)) {
var twitterUser = await twitterService.GetUserByUsername(celeb.Username);
celeb.TwitterId = twitterUser.Data.Id;
}
var currentlyFollowing = await GetCurrentlyFollowing(celeb.TwitterId);
await CheckIfAnyUnfollows(celeb, currentlyFollowing);
await UpdateCeleb(celeb, currentlyFollowing);
}
Getting the people a user follows:
GET https://api.twitter.com/2/users/:userId/following
This returns a list of users along with some metadata, telling us whether there’s any more to request.
You can pass a param to determine the size of the batch, I used 1000 as it’s the max allowed — in an attempt to minimise the number of requests I’m making.
GET https://api.twitter.com/2/users/:userId/following?max_results=1000
If a user follows over 1000 people then the response will contain next_token
, which you simply append in your next request like so:
GET https://api.twitter.com/2/users/:userId/following?max_results=1000&pagination_token=:next_token
Which in code looks something like this:
public async Task<IEnumerable<Followee>> GetPeopleIFollow(string id) {
var client = new HttpClient();
client.DefaultRequestHeaders.Add("Authorization", "Bearer " + Environment.GetEnvironmentVariable("TWITTER_BEARER"));
var response = await client.GetAsync($ "https://api.twitter.com/2/users/{id}/following?max_results=1000");
var data = await response.Content.ReadAsStringAsync();
var followees = new List<Followee>();
var following = JsonConvert.DeserializeObject<Following> (data);
followees.AddRange(following.Data);
while (!string.IsNullOrEmpty(following.Meta.NextToken)) {
var newResponse = await client.GetAsync($ "https://api.twitter.com/2/users/{id}/following?max_results=1000&pagination_token={following.Meta.NextToken}");
var newData = await newResponse.Content.ReadAsStringAsync();
following = JsonConvert.DeserializeObject<Following> (newData);
followees.AddRange(following.Data);
}
return followees;
}
Now we’ve got this information, I map it to a list of strings just containing the ids of the people a celebrity follows, and perform a comparison against the existing data. If a user from the existing list is not in the new list, they’ve been unfollowed.
private async Task CheckIfAnyUnfollows(Celeb celeb, IEnumerable<string> currentlyFollowing) {
if (celeb.Following != null) {
foreach(var wasFollowing in celeb.Following) {
if (!currentlyFollowing.Contains(wasFollowing)) {
var userUnfollowed = await twitterService.GetUser(wasFollowing);
var output = $ "{celeb.Name} (@{celeb.Username}) just unfollowed {userUnfollowed.Data.Name} (@{userUnfollowed.Data.Username}) 🌶🌶";
await twitterService.Tweet(output);
}
}
}
}
And if an unfollow has happened, send a tweet!
For all requests used thus far I’ve utilised a bearer token for the auth, but for sending a tweet oauth 1.0 is required, hence the different setup here:
public async Task Tweet(string message) {
var consumerKey = Environment.GetEnvironmentVariable("TWITTER_API_KEY");
var consumerSecret = Environment.GetEnvironmentVariable("TWITTER_API_KEY_SECRET");
var accessToken = Environment.GetEnvironmentVariable("TWITTER_ACCESS_TOKEN");
var accessTokenSecret = Environment.GetEnvironmentVariable("TWITTER_ACCESS_TOKEN_SECRET");
var client = new HttpClient();
OAuthHeaderGenerator oAuthHeaderGenerator = new OAuthHeaderGenerator(consumerKey, consumerSecret, accessToken, accessTokenSecret);
var url = "https://api.twitter.com/2/tweets";
var oAuth = oAuthHeaderGenerator.GenerateOAuthHeader("POST", url);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("OAuth", oAuth);
var tweet = new Tweet(message);
var json = JsonConvert.SerializeObject(tweet);
var data = new StringContent(json, Encoding.UTF8, "application/json");
await client.PostAsync(url, data);
}
If you’re interested in OAuth, then you’d like PIN-based Oauth!
So there you have it, I now have a bot that will cause some sh*t for me 😂
It’s currently deployed on my Azure subscription at https://spicyapp.azurewebsites.net, but because it’s a pay-as-you-go subscription, I doubt I’ll have it up for more than a week, unless It really starts kicking off.
If you liked this blog then please sign up for my newsletter and join an awesome community!!