Questions? Feedback? powered by Olark live chat software
Ignorar Navegação

Using the Azure WebJobs SDK to create custom Slack integrations

Publicado em 8 março, 2016

Program Manager, Azure App Service

Developers and businesses today are using more SaaS experiences than ever before. Many users of SaaS services quickly find themselves in a position where they need to customize and bring the features of these SaaS services into their line of businesses applications. The first place to look for extending a SaaS service is a PaaS service, like Azure App Service. With Azure App Service, simple yet important features like enterprise grade Authentication or an agile dev ops development cycle work out of the box. Azure, and in general at Microsoft, have tons of ways of integrating with other services; one of the most powerful ways is Azure App Service WebJobs.

In this blog post, we'll explore using a WebJobs Extension to talk to Slack. Slack is a messaging app for teams which has an awesome integration story using their "Real Time Messaging" API or their incoming & outgoing webhooks. Using the WebJobs SDK, we can listen to outgoing and "slash command" webhooks from Slack to initiate processes inside my applications. We can also, from within the SDK, send messages directly into Slack using a Custom WebJobs Extension that I wrote in less than a day.

Let's get to the code.

Get the code for the sample

If you want to follow along in the source code, I encourage you to fork and clone the azure-webjobs-sdk-extensions-slack GitHub repository. You can also learn how to get the NuGet for this sample from the GitHub repo.

Using WebJobs to send Message to Slack WebHooks

To send messages into Slack, it's just an HTTP call to the incoming webhook endpoint, after I've set up that endpoint from the Slack custom integrations menu for Inbound webhooks. Using the Azure WebJobs extension for Slack, we can easily format and send those messages to Slack from within the various automated tasks we've got going for our applications. In some of my teams, we've used Slack to help monitor questions from our various Stack Overflow tags and forums, as well as, customer feedback from User Voice. While there are lots of ways to send data into Slack from services such as IFTTT, whenever some custom code needs to be written, it can be arduous to find a place to host that code. Let's look at how easy it is to send a message to Slack from WebJobs.

To get started using the Azure WebJobs Extension for Slack after we've added the reference to our project, we need to enable it in the WebJobs JobHost configuration.

var config = new JobHostConfiguration();
var slackConfig = new SlackConfiguration();

// These are optional and will be applied if no other value is specified.
slackConfig.WebHookUrl = "";
// IT IS A BAD THING TO HARDCODE YOUR WEBHOOKURL, USE THE APP SETTING "AzureWebJobsSlackWebHookKeyName"
slackConfig.IconEmoji = ":taco:";
slackConfig.Username = "Destroy All Humans";
slackConfig.Channel = "#bots_only_no_hummies";

config.UseSlack(slackConfig);

var host = new JobHost(config);
// The following code ensures that the WebJob will be running continuously
host.RunAndBlock();

Once we've enabled that, we can use the Slack binding within our WebJobs Functions. Getting started using the Slack binding in a function is really simple. All you need to do is add the Slack binding to a WebJob SDK function.

public void SimpleSlackBinding([WebHookTrigger] Message m,
            [Slack(Text = "{Text}", IconEmoji = "{IconEmoji}")] SlackMessage message)

In the above snippet, we've added a Slack binding which will create a SlackMessage with the global default settings and text and iconEmoji over-written with properites from the Message POCO which is generated by our trigger (in this case, a webhook, but it could be any trigger).

The SlackMessage object that is provided is what will be sent to Slack. After the function has finished the, it will parse the content to a JSON body and POST it to your Slack webhook URL, that was specified earlier.

Here's a sample with the full functionality exposed:

public void FullSlackBinding([WebHookTrigger] Message m,
    [Slack(Channel = "{Channel}", 
            IconEmoji = "{IconEmoji}", 
            IsMarkdown = false, 
            Text ="{Text}", 
            Username = "{Username}", 
            WebHookUrl = "{WebHookUrl}")] SlackMessage message,
    TextWriter log
    )
{
    // Further customize Slack Message here. i.e. add attachments, etc.
    // More info on the SlackMessage object on the GitHub project: https://github.com/nerdfury/Slack.Webhooks

    // Continue to set/manipulate properties on the Slack Message, programatically
    message.Text += "WebJobs are Grrrreat!";
    message.IconEmoji = ":troll:";

    // For example, add attachments. See the Slack API docs for more info: https://api.slack.com/docs/attachments
    message.Attachments.Add(new SlackAttachment
    {
        Fallback = "Required plain-text summary of the attachment.",
        Color = "#36a64f",
        pre class="prettyprint"text = "Optional text that appears above the attachment block",
        AuthorName = "Bobby Tables",
        AuthorLink = "http://flickr.com/bobby/",
        AuthorIcon = "http://flickr.com/icons/bobby.jpg",
        Title = "Slack API Documentation",
        TitleLink = "https://api.slack.com/",
        Text = "Optional text that appears within the attachment",
        Fields = new List<SlackField>
        {
            new SlackField
            {
                Title = "Priority",
                Value = "High",
                Short = true
            },
            new SlackField
            {
                Title = "Assigned",
                Value = "Bobby",
                Short = true
            }
        },
        ImageUrl = "http://my-website.com/path/to/image.jpg",
        ThumbUrl = "http://example.com/path/to/thumb.png"
    });
}

Using WebJobs to listen to Slack Webhooks

Sending message to Slack is great, but one of the coolest scenarios is using Slack to initial workflows inside your application. To achieve this with WebJobs, we can use the WebHookTrigger binding. As we can see in the pre class="prettyprint"vious example, we can bind a WebHookTrigger which will output a POCO object for our service to use.

public async Task SlackIniatedWebHook([WebHookTrigger] WebHookContext context

In the code sample above, you can see that we're able to create a webhook in a simple fashion by adding the WebHookTrigger attribute. The path to access this webhook is styled like: https://{uid}:{pwd}@{site}/api/continuouswebjobs/{job}/passthrough/{*path}.

To manually construct your webhook URL, you need to replace the following tokens:

  • uid : This is the user ID from your publishing credentials. You can download your publishing credentials from the portal as detailed in this blog post
  • pwd : This is the password from your publising credentials
  • site : Your SCM site (e.g. myapp.scm.azurewebsites.net)
  • job : The name of your Continuous WebJob
  • path : This is the route identifying the specific webhook function to invoke. By convention, this is {ClassName}/{MethodName}, but can be overridden/specified explicitly via the WebHookTrigger attribute.

Let's say we want to have our webhook listened on slack/webhook. We can do this by passing the path as part of the attribute.

public async Task SlackIniatedWebHook([WebHookTrigger("slack/webhook")] WebHookContext context

The full documentation for the WebHookTrigger can be found on the WebJobs SDK Extensions GitHub Repo. On the GitHub, you can learn the ins and outs of the WebHooks Trigger. We'll expand on this sample in the next section where we bring together incoming and outgoing Slack webhooks.

Bringing it all together: Slack Slash Commands

Let us take a look at what a more end to end situation might look like. We can use a slash command to call a WebJob via the WebHooksTrigger and then kickoff a workflow which we manage via Azure Storage Queues. The the workflow can then respond to the reply url that the slash command provides. Something to keep in mind is that currently the reply url is only valid for about 5 minutes, so if our job is going to run longer than that, we should just use a traditional incoming webhook. In this example, we'll have a slash command which accepts two integers, one which dictates the number of work items and another which is the amount of work to be done.

Let's start with the WebHookTrigger. We want to have the WebHookTrigger listen on slack/webhook and we want to output a set of new Queue messages via a Queue attribtue.

public async Task SlackIniatedWebHook([WebHookTrigger("slack/webhook")] WebHookContext context,
    [Queue("SlackWork")] ICollector<SlackWork> messages
)

With our function parameters and attributes set up, let's think about the busines logic inside the function. We want to read the two integers in from the command text and create the number of queue work items we've been told to create. We'll create a helper POCO class which repre class="prettyprint"sents the work to be done.

public class SlackWork
{
    public int id { get; set; }
    public string username { get; set; }
    public int work { get; set; }
    public string replyUrl { get; set; }
}

We will also create a helper function for parsing Slack since it uses a x-url-form-urlencoded body, and WebJobs doesn't yet auto-parse that into a POCO for us.

private bool TryParseSlackBody(string body, out NameValueCollection nvc)
{
    body = body.Replace('\n', '&');
    body = body.Replace("\r", "");
    nvc = System.Web.HttpUtility.ParseQueryString(body);

    return nvc.Count > 0;
}

Now that we have those, we can write our function to parse the command text, handle error cases, and create the queue work items.

public async Task SlackIniatedWebHook([WebHookTrigger("slack/webhook")] WebHookContext context,
    [Queue("SlackWork")] ICollector<SlackWork> messages
)
{
    // Try and parse the Slack Message Body with simple helper method
    NameValueCollection nvc;
    if(TryParseSlackBody(await context.Request.Content.ReadAsStringAsync(), out nvc))
    {
        Regex rgx = new Regex("(\\d+) (\\d+)");
        Match match = rgx.Match(nvc["text"]);
        int count;
        int work;
        if(int.TryParse(match.Groups[1].Value, out count) && int.TryParse(match.Groups[2].Value, out work))
        {
            for (int i = 0; i < count; i++)
            {
                messages.Add(new SlackWork { id = i, work = work, replyUrl = nvc["response_url"], username = nvc["user_name"] });
            }

            // All good, quickly send an affirmative response
            context.Response = new Httpre class="prettyprint"sponseMessage(HttpStatusCode.Accepted)
            {
                Content = new StringContent("Message received! Processing ...")
            };
        }
        else
        {
            // Not good, quick send a negative response
            context.Response = new Httpre class="prettyprint"sponseMessage(HttpStatusCode.Accepted)
            {
                Content = new StringContent("Incorrect format - please pass two numbers along - i.e. /cmd 2 30")
            };

            // We can stop here for the failure case
            return;
        }
    }
    else
    {
        // Not good, quick send a negative response
        context.Response = new Httpre class="prettyprint"sponseMessage(HttpStatusCode.Accepted)
        {
            Content = new StringContent("Something went wrong. :(")
        };

        // We can stop here for the failure case
        return;
    }


}

Now for processing the work items, we need to have a QueueTrigger which will launch on new Queue items. Our function will sleep for the amount of "work" passed to it (plus some randomness factor we'll add), if the work to be done is greater than 10, we'll change the Slack emoji to :tada:. We can create the text for the Slack message via the function parameters and attribtues.

public void ProcessSlackWork([QueueTrigger("SlackWork")] SlackWork work, 
    [Slack(WebHookUrl = "{replyUrl}", Text = "Item: {id} finished processing {work} seconds of work.", IconEmoji = ":sleepy:", Channel = "@{username}")] SlackMessage slack,
    TextWriter log
)
{
    log.WriteLine($"Processing id: {work.id} - working for {work.work} seconds");


    if(work.work > 10)
    {
        slack.IconEmoji = ":tada:";
    }

    int sleepFor = (work.work + (int)(.2 * work.work * (new Random()).NextDouble())) * 1000;
    log.WriteLine($"Processing id: {work.id} - actually working for {sleepFor} seconds, because of some made up, factor of error");
    Thread.Sleep(sleepFor);
}

We can test this locally by running the WebJob and hitting localhost:3000/slack/webhook. You can pass a sample body that looks something like below:

token=gIkuvaNzQIHg97ATvDxqgjtO
team_id=T0001
team_domain=example
channel_id=C2147483705
channel_name=test
user_id=U2147483697
user_name=Steve
command=/cmd
text=3 30
response_url=https://hooks.slack.com/commands/1234/5678

Next, we deploy the WebJob to an Azure App Service. To do this, we can follow the steps for deploying a WebJob to Azure via Visual Studio on Azure.com.

Finally, let's add the slash command. Go to the Slack apps directory and click on "configure" in the top right. From here, you need to select your Slack team, and then navigate to "Custom Integrations". From this menu, you can navigate to "Slash Commands" and click on "Add Configuration". Follow the steps on the page and add a URL with the pattern described in the section above. You can use whatever /command keyword you'd like to.

Now you can call your WebJob by calling your command with two integers representing the amount of work to be done and how long to work: /cmd 3 30.

Learn more

To learn more, check out the following links: