My SlackBot

Slack is a wonderful collaboration tool for uniting team communication. I loved the idea of using Slack to automate many of my tasks. As a Software Engineering Director of a small company, one is expected to wear many hats, and I wanted to see what of the many patterned, “repeatable” tasks I could automate for my team.

As mentioned previously, I work for a small company. This tends to insinuate that we do not have the funds to dump into enterprise software, and also means that as employees of the company, we need to get creative. Back in my undergraduate days, I was the president of the Linux User’s Group and helped pioneer an open-source FOSS Conference called Flourish!. I was very much into the free-use and free-pay model of software. Open-source and open use. Tapping into this idea, our group decided to start out with Trello and then move to Taiga for our project management platform.

Taiga is a free project management platform when hosted internally. It has a lovely UI and is like a more basic view of JIRA. It also comes with a nice API. I decided that I would use this API to not only generate my own metrics reports (for another post), but also to create a sort of “Scrum Master” bot that could remind the team of important issues or stale user stories as we neared the end of a sprint.

The technology we use for the “middle-layer” of our application stack is ASPNET C#. I thoroughly enjoy the framework, in particular .NET Core. My SlackBot application is written as an API that runs on a server with a set of “Helper” classes, allowing me to use SlackBot in a variety of migration scripts (to update users when a particular migration has completed,for example).

The use of “async” is a relatively new feature, introduced somewhere around version 5 of C#. I wanted to ensure that I could integrate SlackBot into some legacy code, if need be. The legacy code at the company was very old, much of it not refactored for years (can you say .vb code?), and it was not worth the technical debt to rewrite any of it, since we had a long-term plan to deprecate as much of it as possible, albeit organically and in time. As such, I wrote both asynchronous and synchronous versions in my SlackBot class.

SlackBot is a static class that lives in a referenced library of any project that will use it. The helper project is written in .net standard, allowing for it to be referenced by both .Net Core and ASPNET frameworks. 

using Newtonsoft.Json;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace Anderson.Core.MrTechBot
{
    public static class SlackClient
    {
        public static async Task<HttpResponseMessage> SendMessageAsync(string webhook, string message)
        {
            HttpClient _httpClient = new HttpClient();

            var payload = new
            {
                text = message
            };
            var serializedPayload = JsonConvert.SerializeObject(payload);
            var response = await _httpClient.PostAsync(webhook,
                new StringContent(serializedPayload, Encoding.UTF8, "application/json"));

            return response;
        }

        public static HttpResponseMessage SendMessage(string webhook, string message)
        {
            HttpClient _httpClient = new HttpClient();

            var payload = new
            {
                text = message
            };
            var serializedPayload = JsonConvert.SerializeObject(payload);
            var response = _httpClient.PostAsync(webhook,
                new StringContent(serializedPayload, Encoding.UTF8, "application/json")).Result;

            return response;
        }

        public static void SendToMrTechBot(string webhook, string message)
        {
            var response = SendMessage(webhook, message);
        }

        public static async Task SendToMrTechBotAsync(string webhook, string message)
        {
            var response = await SendMessageAsync(webhook, message);
        }
    }
}

The code above was really all that was needed. Next we move along to the application logic that utilized this class.

The application must first explicitly define the objected used by the API. I defined those with a set of models that mapped directly onto the Taiga API per the documentation. Using a singleton HttpClient (very important for performance and general connection cleanup), the application polls the Taiga API every few minutes and sends along any updates.

First, I wanted SlackBot to tell all users if any NEW issues had been assigned to them since the previous day. I wanted this to happen only once and in the morning. The user notified would then be prompted to at least address the new issue; the status of the issue should be changed from NEW to ToDo (after it had been scoped out or readied for Planning Poker, or reassigned). To do this, I dedicated a single field in a datatable on a small MariaDB database that kept track of whether or not a ping had been sent for that day. If the flag was true, no matter how many times the endpoint was called, the users would not be notified again.

public static void UpdateSendReportFlag(int bit)
        {
            using (var _db = new MySqlConnection(CONNECTION_STRING))
            {
                string sql = @"update MrTechBot set SentToday=@bit";
                using (var cmd = new MySqlCommand(sql, _db))
                {
                    _db.Open();
                    cmd.Parameters.AddWithValue("@bit", bit);
                    cmd.ExecuteNonQuery();
                    _db.Close();
                }
            }
        }

There is also a GET block that is called inside a while loop within the main program. The code block above is how I generally approach the service-layer logic (the “repository) for fetching data. I separate these calls into their own services so that I can connect any sort of API or data mocker to it. The above uses raw MySqlCommands, but I have also used Dapper (small and mighty but limited) as well as Entity Framework (which can almost be “too convenient” in that you’re not really writing SQL).

The while loop uses the Taiga API to authenticate, grab all issues, user stories, users, and projects, along with the various severity and priority levels, as well as classification (bug, enhancement, etc). This is all compiled into “UserReport” objects that hold all relevant objects pertaining to the user, and because I follow the same convention across projects, I can easily compile a person’s issues and user stories across all simultaneous projects (something the API does not support as of this post).

The UserReports are compiled and then a message composed and sent to the relevant user. Users are stored in a dictionary of TaigaUserName and SlackName. Because this was an internal piece of software, I simply had a two-column table in MariaDB representing my dictionary. The more correct way would be for the SlackBot to authenticate the user to the application and do the linking for the user. It would still be in database, but for my own purpose I inserted these manually.

 public int ProcessBot(Taiga t)
        {
            var mr_techbot_channel = "https://hooks.slack.com/services/<channel_hash>";
            userTaigaMap.Add("jenanderson", mr_techbot_channel);
            //...

            foreach (var user in t.TaigaUsers)
            {
                //post to private channel
                if (userTaigaMap.Keys.Contains(user.Value.UserName.ToLower()))
                {
                    if (userTaigaMap.Any(x => x.Key.Equals(user.Value.UserName.ToLower())))
                        taigaSlackUsers.Add(new TaigaSlackUser() { Webhook = userTaigaMap[user.Value.UserName.ToLower()], username = user.Value.UserName, Issues = issues });
                }
            }
            Task.WaitAll(IntegrateWithSlackAsync(taigaSlackUsers));
            return 1; //return 1 so know block has completed
        }

Finally, we compose the message:

 private static async Task IntegrateWithSlackAsync(List<TaigaSlackUser> summ)
        {
            TimeSpan start = new TimeSpan(8, 0, 0); //10 o'clock
            TimeSpan end = new TimeSpan(10, 0, 0);
            TimeSpan now = DateTime.Now.TimeOfDay;
            Dictionary<string, string> emojiPriority = new Dictionary<string, string>();
            emojiPriority.Add("high", "🔴");
            emojiPriority.Add("normal", ":green-circle:");
            emojiPriority.Add("low", "⚪");
            if ((now > start) && (now < end) && DateTime.Today.DayOfWeek != DayOfWeek.Saturday && DateTime.Today.DayOfWeek != DayOfWeek.Sunday)
                {
                if (GetSendReportFlag() == 0)
                {
                    foreach (var itm in summ)
                    {
                        StringBuilder builder = new StringBuilder();

                        builder.AppendLine("*Hi <@" + itm.username + ">!* :wave:");
                        builder.AppendLine("Please take action on the following NEW issues:");

                        foreach (var i in itm.Issues)
                        {
                            builder.AppendLine(emojiPriority[i.Priority.ToLower()] + "*" + i.ProjectName + " | " + i.Subject + "*");
                            builder.AppendLine("><URL_TO_HOSTED_TAIGA>" + i.Slug + "/issue/" + i.Reference);
                        }

                        if (itm.Issues.Count > 0)
                        {

                            var slackClient = new SlackClient(new Uri(itm.Webhook));

                            var response = await slackClient.SendMessageAsync(builder.ToString());
                        }
                    }
                    UpdateSendReportFlag(1);
                }
            }
            else
            {
                if (GetSendReportFlag() == 1)
                {
                    UpdateSendReportFlag(0);
                }
            }
        }
    }

And there is my basic SlackBot!

Many improvements could be made to this code, and it has yet to be refactored. It started as a fun side-project and some shortcuts were taken for the sake of production. I would prefer to get more sophisticated with the polling logic I’ve instituted. This would be a nice opportunity to integrate SignalR and perhaps Redis for the very basic DB functionality I implement. I would also like to create user interactions with the bot allowing the user to ping SlackBot when they wanted to know of any new issues, or they could ask questions regarding a specific User Story or Project.