Integrating Plastic SCM with Slack

Tuesday, February 03, 2015 0 Comments

As you know Plastic SCM has awesome features built in but it is challenging to address users every need in a product of this type. So with that in mind, Codice Software created an automation layer called CmdRunner that allows you to easily and quickly query large amounts of information. The best way to show you how this works will be by example so we have chosen to integrate Plastic SCM with the Slack Bot API. We will use this example throughout this document to demonstrate all of the steps necessary to perform this integration.

Slack

Slack is a tool that encourages better communication, collaboration and sharing of content among team members by providing a platform that puts everything in one place, is instantly searchable and is available wherever you are. The tool delivers all this by utilizing a web interface and apps for mobile platforms such as iOS, Android and soon for Windows. It also delivers a powerful public API that allows third parties to develop new ways to use Slack. Those API's range from Web services to Real time messaging and even includes Bot integration.

Getting Started

In order to integrate Plastic SCM with Slack you will need to perform a few simple steps as follows:

  1. Create a new Bot user in your Slack team account which will create a token to use with the Bot API
  2. Use the Bot API token to start a new real time messaging (RTM) session which will deliver a WebSocket address that you can connect to
  3. Connect to the RTM WebSocket in order to start receiving the activity from your team. You will need to ping the Slack server regularly to maintain an active connection
  4. Use CmdRunner to query Plastic SCM for information to send to Slack either directly or by utilizing a predefined channel dedicated to query for that information

Now that you have a basic understanding of what is involved, let’s take a closer look at each step.

Creating a new Slack bot

On the Slack Integrations configuration page go to "DIY Integrations & Customizations" and press the "Add" button on Bots integration like this:

After adding the Bots integration, Slack will ask you to enter a name for your Bot like this:

Once you have entered the name for your Bot, Slack will display your Bot configuration where you will find the API Token you will need to start a Real Time Messaging session using your Bot. You can also configure your Bot name, define an icon and include a description like this:

Now that you have your Bot created, let’s go to the next step.

Starting a Real time messaging session from your code

The best way to illustrate how you start a real time messaging session is to use an example. In this solution example we will use a WPF application, written in C# and using a MVVM pattern. In the solution, you will find two projects. The first is the SlackBot (the WPF app) and the second is the Slack.API (the API implementation).

Taking a look at the Slack.API project you will notice that there is a Services folder with two services in it called SlackRTMService and PlasticCMDService. For this solution example we will first start working with the SlackRTMService.

Note: Each service is composed of two file types; Interface and class. These file types will become useful later when you are using dependency injection to inject every service implementation (class) into your ViewModel based only on the service definition (interface).

Going back to the SlackRTMService interface, you will notice that it exposes one method identified as ConnectPlasticSlackBot and one event identified as SlackDataReceived like this:

public interface ISlackRTMService
{
    event EventHandler SlackDataReceived;

    void ConnectPlasticSlackBot(string botToken);
}
Note: This solution is a very simple example of integration and in a Real World™ application you would typically be dealing with many more methods/events interacting with Slack or any other API.

So for this solution example we will focus only on the ConnectPlasticSlackBot method. As we stated earlier, the first thing we need to do is use the Bot API token to start a new real time messaging (RTM) session which will deliver a WebSocket address that you can connect to. To do this you simply issue a GET petition to the rtm.start method of the Slack API and pass the Token that you obtained through the regular user authentication or that you copied from Bot dashboard like this:

https://slack.com/api/rtm.start?token=[YOUR_TOKEN_GOES_HERE]

To issue the GET petition you can use the HttpWebRequest class like this:

private const string RtmStart = "https://slack.com/api/rtm.start?token={0}";

public void ConnectPlasticSlackBot(string botToken)
{
    HttpWebRequest rtmStartRequest = (HttpWebRequest)HttpWebRequest.Create(
                                          new Uri(string.Format(RtmStart, botToken)));

    var rtmResult = rtmStartRequest.GetResponse();
    using (var stream = new StreamReader(rtmResult.GetResponseStream()))
    {
        string responseString = stream.ReadToEnd();

        if (!string.IsNullOrEmpty(responseString))
        {
            //Do something awesome
        }
    }
}

After reading the result stream you are going to get a JSON string, that looks something like this:

If you would like to take a look at the JSON received and interpreted by Visual Studio Quick Watch feature you may do so but due to the very large size of the total JSON string it was not included here. Just be aware that the most important property in this JSON is the blue outlined area designated in the screen shot above where url: refers to the url of the WebSocket that this session created.

Now that you have the JSON string, you can use the JSON.NET utilities to map the JSON string to a class. In our example, you will notice that there is a class called RtmStartResponse in the Slack.API project Model folder like this:

public class RtmStartResponse
{
    [JsonProperty("ok")]
    public bool Ok { get; set; }

    [JsonProperty("url")]
    public string RtmUrl { get; set; }
}
Note: in our example we only map the “ok” and “url” fields but you can map all of them by simply adding new properties with the [JsonProperty] attribute.

You are now ready to use the DeserializeObject method from the JsonConvert class to map the JSON string to a new instance of RtmStartResponse class like this:

this.rtmStartResponse = JsonConvert.DeserializeObject(responseString);

You can now connect a web socket to the url stored in RtmUrl property of RtmStartResponse class. You will need to be fast on your feet because that url is going to be live for only 30 seconds. If you don’t ping the server before time is up the url is dismissed and marked as invalid at server side.

The best way to connect to web sockets is by using a third party library like WebSocketSharp which makes the process very easy like this:

private void ConnectWebSocket(RtmStartResponse rtmStartResponse)
{
    if (rtmStartResponse == null || string.IsNullOrEmpty(rtmStartResponse.RtmUrl))
        return;

    WebSocketSharp.WebSocket socket = new WebSocketSharp.WebSocket(rtmStartResponse.RtmUrl);
    socket.OnMessage += socket_OnMessage;
    socket.OnOpen += socket_OnOpen;
    socket.Connect();
}

Once you have created a new WebSocket instance using the given url as the parameter then you can subscribe to "connection open event" and "message received event" which will allow you to invoke Connect method from the created instance to launch the web socket connection. You are now ready to start receiving and sending messages to Slack, the heart of your Bot.

Communicating with Slack

After connecting with the Slack web socket, you will notice that the OnOpen event will be raised. This event is designed to set up an event timer to regularly ping the Slack service and maintain an open connection like this:

void socket_OnOpen(object sender, EventArgs e)
{
    var socket = (WebSocket)sender;
    this.timer = new Timer((state) =>
    {
        socket.Send(string.Format(JsonOpenResponse, DateTime.Now.Ticks));
    }, null, 0, TimeInterval);
}

Using the event timer you can send a ping request to the Slack server every 25 seconds that will maintain an open connection. The event timer expects to receive a pong response message from the Slack server which ensures that the connection will remain active.

Note: as you can see in our example we used DateTime.Now.Ticks as the ID in all of our petitions to the server. We did this because Slack requires a unique ID integer for every petition or message.

You are now ready to start receiving Slack messages using the OnMessage event handler that you setup before connecting to the service. Again, the format of the date you receive is a json serialized string so you will need to create a class to handle it similar to the SlackMessage class like this:

public class SlackMessage
{
    [JsonProperty("id")]
    public string Id { get; set; }

    [JsonProperty("type")]
    public string Type { get; set; }

    [JsonProperty("channel")]
    public string Channel { get; set; }

    [JsonProperty("user")]
    public string User { get; set; }

    [JsonProperty("text")]
    public string Text { get; set; }

    [JsonProperty("ts")]
    public string Ts { get; set; }

    [JsonProperty("team")]
    public string Team { get; set; }
}

At this point you are now ready to actually start integrating Slack with your systems such as Plastic SCM like this:

void socket_OnMessage(object sender, WebSocketSharp.MessageEventArgs e)
{
    var socket = (WebSocket)sender;

    var receivedResponse = 
                          JsonConvert.DeserializeObject(e.Data);

    //Show we have a new message...
    if (SlackDataReceived != null)
    {
        SlackDataReceived(this, new SlackEventArgs(receivedResponse));
    }

    //Then continue processing the message...
    if (IsInvalidResponse(receivedResponse.Text))
        return;

    ProcessMessageForPlastic((WebSocket)sender, receivedResponse);
}

When you start receiving new data from Slack the first thing you need to do is deserialize the JSON string to a new instance of the SlackMessage class. You will then test the deserialization and if you were successful you will now have a valid data instance enabling the SlackRTMService to raise the SlackDataReceived event. This will notify the UI or other parts of the application that you are successfully receiving data.

Now that you have set up a valid data instance and you are successfully receiving data you need to disable the pong response to the ping petitions to the Slack server and ensure that you are processing valid text that is not "null" or empty. It is a good idea to test that the text is valid before proceeding any further.

Assuming that the text is valid, the last step is to check to make sure that the text does not start with the word plastic as this word could be misinterpreted as a command for the Plastic SCM Bot so we recommend that you implement a validation using the IsInvalidResponse method like this:

bool IsInvalidResponse(string receivedResponse)
{
    return string.IsNullOrEmpty(receivedResponse)
        || receivedResponse == "pong"
        || !receivedResponse.ToLowerInvariant().StartsWith("plastic");
}

Processing the command

We are now ready to use our Bot to search for information using two commands that that we defined that will enable our Bot to search Plastic SCM and find the latest branches and latest changesets. Since these commands could take quite a bit of time to execute to retrieve all of the information from Plastic SCM we recommend that before you initiate a ProcessMessageForPlastic, you setup a invalidMessage variable in case the command is not recognized and a waitmessage that will automatically inform the user that the petition is active and working like this:

string InvalidMessage = @"Could you ask again? I didn't understand you :pensive:
                        These are the commands I understand:
                        > `plastic latest changesets`
                        > `plastic latest branches`";
string WaitMessage = @"As you wish, my lord.
                     Please wait while I gather the required information.
                     I assure you, this will only take a moment. :suspect:";
SendMessage(socket, messageReceived, waitMessage);

The SendMessage method will create a new instance of the SlackMessage class using the received message data for destination channel and user, and specifying message as the type of operation so that Slack knows what text to print in the specified channel like this:

private static void SendMessage(WebSocket socket, SlackMessage messageReceived, string cmdResult)
{
    SlackMessage response = new SlackMessage()
    {
        Id = DateTime.Now.Ticks.ToString(),
        Channel = messageReceived.Channel,
        User = messageReceived.User,
        Type = "message",
        Text = cmdResult
    };

    string msg = JsonConvert.SerializeObject(response);
    socket.Send(msg);
}

The result in the Slack chat window is going to be something like this:

As you can see the message informs you that the information you requested is being gathered and you will have all the time you need to query for the specific information. To do this in our example you will use another service called PlasticCMDService where the Plastic SCM CMDRunner API is used to query the data you want to show.

Interacting with Plastic SCM API: CmdRunner

Plastic SCM has an awesome command line tool that allows you to query a lot of information which makes this tool very useful. To set it up to automatically extract the console printed information and use it in your custom application will require the use of CmdRunner. This API was developed by Codice Software for Plastic SCM and it allows you to interact from C# with the command line and get the results very easily.

To start working with CmdRunner, first thing is to clone the repository from GitHub. If you do not have GitHub, you can download it from this location: https://github.com/PlasticSCM/plastic-cmdrunner.

After the cloning of the repository is complete you will need to open the CmdRunner project in Visual Studio and compile it. You can then grab the compiled DLL file and add it to your project as a reference. It is as simple as that to add CmdRunner to your code.

Referring to the PlasticCMDService in our example, you will notice that there are two methods identified as GetLatestChangesets and GetLatestBranchers like this:

public class PlasticCMDService : IPlasticCMDService
{
    private string repository = "default";
    public string GetLatestChangesets()
    {
        string command = string.Format(@"cm find changesets on repositories '{0}' 
                                          --format={{date}}#{{comment}} --nototal", repository);
        string cmdResult = CmdRunner.ExecuteCommandWithStringResult(command, Environment.CurrentDirectory);
        cmdResult = string.Format("Latest changesets on {0} repository: \r\n {1}", repository, cmdResult);

        return cmdResult;
    }

    public string GetLatestBranches()
    {
        string command = string.Format("cm find branch on repositories '{0}' 
                                               --format={{id}}#{{name}} --nototal", repository);
        string cmdResult = CmdRunner.ExecuteCommandWithStringResult(command, Environment.CurrentDirectory);
        cmdResult = string.Format("Latest branches on {0} repository: \r\n {1}", repository, cmdResult);

        return cmdResult;
    }
}

These methods are actually identical and only differ in the command line arguments that define the specific information that will be returned (changesets or branches). You always start with cm, the Plastic SCM CLI tool that will execute searches in Plastic SCM using the find command. Using the first method as an example, you would be searching for changesets on repositories so you would need to point to the repositories you want to search. You would then add the formatting argument to tell the Plastic SCM CLI tool how to format the data of the output text. Let’s take a look at each segment of the command line arguments like this:

Command line part Description
cm The tool to execute in the command line.
find The method in plastic to execute, in this case you want to find information.
changesets / branch What kind of information you want to look for.
on repositories This parameter indicates you want to search this information, not in your workspace but in the repository indicated after it.
'{0}' C# string.format parameter placeholder, this is going to be replaced by repository variable content. In this case is default, the name of the default repository.
--format= This indicate you are going to define the output format for the data.
{{date}}#{{comment}}/{{id}}#{{name}} The data format you want, name of the field to display, between {{}} and separator to use #
--nototal Don't show the total count of data.

For a complete guide on Plastic SCM CLI API, you can take a look at this page of the Plastic SCM web site.

Now that you have defined your command line to query Plastic SCM, you will need to use the CmdRunner class to execute it. The CmdRunner class has several methods that can be used to execute commands with or without output. Your choices include; with string output, asking for user input, to execute the shell or to terminate it. In our example you are going to use the method that will return data as a string by using the command ExecuteCommandWithStringResult. This method will require two parameters:

  1. The command string to execute,
  2. The folder where execution is made.

For the working folder you can use the one that contains the application execution. This is very useful if you want to save information directly to an XML file.

After calling the ExecuteCommandWithStringResult, CmdRunner is going to execute the command string in a command line window and return a string with the results returned by Plastic SCM. For example, in the case of the changesets command, you are going to get something like this:

1/22/2015 1:51:20 PM#Type your comments here 
1/22/2015 1:52:22 PM#Deleted not needed files 
1/22/2015 2:42:35 PM#Added first steps for connecting with Slack in article. Also added images. 
1/22/2015 5:19:39 PM#Added another image and more text to article 
1/22/2015 5:24:24 PM#Updated articles backlog 
1/23/2015 4:08:57 PM#More text added to article

Now all you need to do is create a new message, using the SendMessage method you saw earlier.

Note: one thing to keep in mind is Slack API limits. You can only post a message of up to 800 characters to the RTM API, so is a good idea to take a look at the result length and cut it if needed.

You are now ready to complete the final code of the ProcessMessageForPlastic like this:

private void ProcessMessageForPlastic(WebSocket socket, SlackMessage messageReceived)
{
    SendMessage(socket, messageReceived, WaitMessage);

    string cmdResult = QueryServer(messageReceived.Text);

    SendMessage(socket, messageReceived, TrimResponse(cmdResult));
}

private string QueryServer(string requestedCommand)
{
    if (requestedCommand.ToLowerInvariant().Contains("latest branches"))
        return this.plasticService.GetLatestBranches();

    if (requestedCommand.ToLowerInvariant().Contains("latest changesets"))
        return this.plasticService.GetLatestChangesets();

    return InvalidMessage;
}

private string TrimResponse(string response)
{
    if (response.Length <= 800)
        return response;
    string result = response.Substring(response.Length - 796, 796);
    result = result.Substring(result.IndexOf("\n"));
    return result;
}

If you successfully execute a valid command, the result in Slack is going to look something like this:

And:

Conclusion

You have now successfully created a working Bot integrated with Plastic SCM and Slack that you can share with your team so that they can use it to query Plastic SCM for the latest changesets, latest branches or any other information you want to make available to your team via Slack.

If you like what you see and you want to download the source code for this example you can press here. If you want to learn more about Plastic SCM then go to the website to take a closer look.

0 comentarios: