I’ve found Twitter is a great way to engage with the .NET community as a whole. Engaging in discussions, sharing ideas and inspiration, and even disagreeing can be a fun part of being part of the tech community. That said, there are a lot of ways you can use the Twitter API to get more from your time on the social network. I use the .NET package LinqToTwitter to share images and tweets with friends on a timed interval, and help promote this very blog you’re reading right now.

One thing I’ve wanted to do for a while is to understand engagements in the context of a single tweet. In this post, I’ll show you how to use LinqToTwitter to get all users who liked or retweeted a particular tweet.

The Anatomy of a Tweet

For folks that are not familiar with Twitter, it is composed of a timeline of messages, also known as tweets. These tweets can have a number of different engagements from followers, typically in the form of likes and retweets. The more engagement gets, the more likely other folks will interact with the tweet. If you’re looking to increase engagement on Twitter, you might want to see which of your tweets get the most engagement and from whom.

In my case, I’m toying around with the idea of raffles and giveaways as a way to make it exciting for people following me on the social media platform.

The Twitter API

The Twitter HTTP API is vast in its scope, and initially looking at it, you’ll notice a lot of functionalities present. If you’re an experienced developer, you’ll also notice the tell-tale signs of an eventually consistent architecture, where like and retweet endpoints are typically separate from the tweet endpoint. In this post, we’ll be focusing on the Liking Users (/2/tweets/{id}/liking_users) and Retweeted By (/2/tweets/{id}/retweeted_by) endpoints. Before diving into the Twitter API, I also recommend reading the Fundamentals documentation, especially around pagination. Finally, be aware of rate limits, as they differ on each endpoint and typically are calculated based on a 15-minute window. For my use case, unless a tweet goes viral, I shouldn’t have any issue calling any endpoint, but it is a good idea to be aware of failure scenarios.

The Code

Linq To Twitter covers the majority of API endpoints provided by Twitter, but for my use case I decided to use the Raw Queries approach. Both endpoints mentioned in the previous section return similar responses, and I would it was much simpler to use the same approach for both rather than having two separate implementations.

I decided to write some extension methods off of the TwitterContext class, since my other uses of the Twitter API use the LINQ syntax provided by the NuGet package. That said, the code could be modified to work directly with an HttpClient.

using System.Text.Json;
using System.Text.Json.Serialization;
using LinqToTwitter;

namespace TwitterRaffle;

public static class TwitterContextExtensions
{
    public static async Task<EngagementResults> GetEngagementsByTweet(this TwitterContext context, ulong tweetId)
    {
        var likes = await GetLikesByTweet(context, tweetId);
        var retweets = await GetRetweetsByTweet(context, tweetId);

        var users = likes.Users.Union(retweets.Users)
            .GroupBy(x => x.Id)
            .Select(group => new User(
                    group.Select(x => x.Id).First(),
                    group.Select(x => x.Name).First(),
                    group.Select(x => x.Username).First(),
                    // account for likes and retweets by the same user
                    group.SelectMany(x => x.Engagements).ToList()
                )
            )
            .ToList();

        return new EngagementResults(users);
    }
    public static async Task<EngagementResults> GetLikesByTweet(this TwitterContext context, ulong tweetId)
    {
        return await GetQueryResults(context, $"/tweets/{tweetId}/liking_users", EngagementType.Like);
    }
    public static async Task<EngagementResults> GetRetweetsByTweet(this TwitterContext context, ulong tweetId)
    {
        return await GetQueryResults(context, $"/tweets/{tweetId}/retweeted_by", EngagementType.Retweet);
    }
    
    private record Result(List<ResultItem> Data, Meta Meta);
    // ReSharper disable once ClassNeverInstantiated.Local
    private record ResultItem(string Id, string Name, string Username);
    private record Meta([property: JsonPropertyName("next_token")] string? NextToken);

    private static async Task<EngagementResults> GetQueryResults(
        TwitterContext context, 
        string originalQueryString,
        EngagementType engagementType)
    {
        // todo: fix this when bug is fixed
        var baseUrl = context.BaseUrl;
        context.BaseUrl = context.BaseUrl2;

        var users = new List<ResultItem>();
        var nextToken = string.Empty;

        while (true)
        {
            var currentQuery = string.IsNullOrEmpty(nextToken)
                ? originalQueryString
                : $"{originalQueryString}?pagination_token={nextToken}";

            var json = await (from raw in context.RawQuery where raw.QueryString == currentQuery select raw)
                .SingleOrDefaultAsync();

            var result = json?.Response is not null
                ? JsonSerializer.Deserialize<Result>(json.Response,
                    new JsonSerializerOptions
                    {
                        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
                        PropertyNameCaseInsensitive = true
                    })
                : new Result(new List<ResultItem>(), new Meta(null));

            if (result?.Data?.Any() == true) {
                users.AddRange(result.Data);
            }

            nextToken = result?.Meta.NextToken;

            // reached the end
            if (nextToken is null)
                break;
        }

        context.BaseUrl = baseUrl;

        // combine, distinct, and return
        return new EngagementResults(users
            .DistinctBy(x => x.Id)
            .Select(x => new User(
                x.Id,
                x.Name,
                x.Username,
                new List<EngagementType> { engagementType })
            )
            .ToList());
    }
}

public record User(
    string Id,
    string Name,
    string Username,
    List<EngagementType> Engagements);

public enum EngagementType
{
    Like,
    Retweet
}

public record EngagementResults(List<User> Users)
{
    public int TotalLikes => Users.Count(x => x.Engagements.Contains(EngagementType.Like));
    public int TotalRetweets => Users.Count(x => x.Engagements.Contains(EngagementType.Retweet));
    public int TotalUsers => Users.Count;
}

In most cases, the code above will make two HTTP calls; one for likes, and the other for retweets. In the case of a popular tweet, you may see multiple calls for each.

You’ll need the following NuGet Packages first:**

  • LinqToTwitter
  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.Configuration.UserSecrets

Let’s see the extension method in use within a console application.

using LinqToTwitter;
using LinqToTwitter.OAuth;
using Microsoft.Extensions.Configuration;
using TwitterRaffle;
using static System.Console;

var configuration = new ConfigurationBuilder()
    .AddUserSecrets<Program>()
    .AddEnvironmentVariables()
    .Build();

const long tweetId = 1510970237251989513;

var twitter = new TwitterContext(
    new SingleUserAuthorizer
    {
        CredentialStore = new SingleUserInMemoryCredentialStore
        {
            ConsumerKey = configuration["ConsumerKey"],
            ConsumerSecret = configuration["ConsumerSecret"],
            OAuthToken = configuration["OAuthToken"],
            OAuthTokenSecret = configuration["OAuthTokenSecret"],
            ScreenName = "buhakmeh"
        }
    }
);

var engagements = await twitter.GetEngagementsByTweet(tweetId);

Clear();
WriteLine($"Total Users: {engagements.TotalUsers}");
WriteLine($"Total Retweets: {engagements.TotalRetweets}");
WriteLine($"Total Likes: {engagements.TotalLikes}");
WriteLine();

engagements
    .Users
    .ForEach(user => WriteLine($"* @{user.Username} ({string.Join(", ", user.Engagements.Select(Emoji))})"));
    
static string Emoji(EngagementType engagementType)
{
    return engagementType switch {
        EngagementType.Like => "❤",
        EngagementType.Retweet => "♺",
        _ => throw new ArgumentOutOfRangeException(nameof(engagementType), engagementType, null)
    };
}

When running the console application, we get the expected result of counts and users.

Total Users: 254
Total Retweets: 48
Total Likes: 243

* @MenoHinojos (❤, ♺)
* @bil_martin (❤)
* @arshadbadarkhan (❤)
* @billseipel (❤)
...

Conclusion

This is a cool use of the Twitter APIs that can help increase your engagement with followers. You could take these users and give them a special shout out, or send them prizes. You can also expand the API requests to include more information on users, like their region, follower count, and many more data points. I hope this code helps you learn more about the Twitter API and to try new and interesting ways to engage with your audience.