Get your copy of The State of Jamstack 2020 Report! Get It Now
blog Integrations

Keep Remote Content Editors Up to Date with Webhooks and Slack

By Eric Dugre Apr 7, 2020

In these tough times when people are working from home, it has become hard to keep every member of your team updated on all the news. People are overwhelmed with emails, and many important messages can disappear unnoticed. What if we used Slack and posted news automatically once they are published in Kontent?

In this article, I’ll show you how to use webhooks to automatically post Slack messages once a content item is published.

A big part of keeping teams updated is communication. More and more, companies are relying on instant messaging to inform team members of updates, meeting requests, and that there is cake in the breakroom. With Slack being one of the most popular clients, I wanted to show you how you can leverage Kontent’s webhook support with your team’s communication platform.

Defining the Process

The first step in the process is to define what your integration looks like. Because every company and project is different, how you connect your systems all depends on your requirements. In my case, I wanted to post a message to a channel when a content item was published. This process would help my marketers stay up to date on new content. This could be especially helpful if you have several external editors who are creating and publishing content within your project. Here is a workflow for my functionality.

Creating the Webhook

With the workflow defined, I was ready to create my integration. With webhooks, I needed a serverless function that was available for Kontent to call when content was updated. This could have been a route/function within an MVC application, an Amazon Lambda function, an Azure Function, or a similar function from another provider. Because Azure Functions have configuration specific for this need, it was a good fit for my solution.

Let’s take a look step by step how the integration in Azure Functions works. First, we need to create a new Http trigger function:

For the configuration, leave the defaults. We can find out the function’s public URL address by clicking the “</> Get function URL” link in Azure Functions. We’ll need it shortly.

Configuring Kentico Kontent

The next step is to configure the webhook within Kentico Kontent. Go into Project settings and create a new webhook. Select Publish in the “Delivery API Triggers” section:

Note that we will need the Secret value later in the process.

Creating the Slack webhook

In your Slack channel, add the Incoming webhooks app to post messages from your Azure Function:

Once it’s created, you can find your Webhook URL in the “Integration Settings” menu.

Adding the Kontent Integration

With everything set up, we’re now ready to add code to the Azure Function. First, we need to create a few files. Do this by expanding the files viewer.

Add the following files:

  • function.proj
  • slackClient.csx

In function.proj, we need to add a reference to Kentico.Kontent.Delivery:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Kentico.Kontent.Delivery" Version="12.3.0" />
    </ItemGroup>
</Project>

This slackClient.csx is a simple class that will perform a POST request to Slack to send a message:

using System.Text;
using System.Net;
using Newtonsoft.Json;
using System.Collections.Specialized;
 
//A simple C# class to post messages to a Slack channel
//Note: This class uses the Newtonsoft Json.NET serializer available via NuGet
public class SlackClient
{
    private readonly Uri _uri;
    private readonly Encoding _encoding = new UTF8Encoding();
 
    public SlackClient(string urlWithAccessToken)
    {
        _uri = new Uri(urlWithAccessToken);
    }
 
    //Post a message using simple strings
    public string PostMessage(string text, string username = null)
    {
        Payload payload = new Payload() {
            Username = username,
            Text = text
        };
 
        string payloadJson = JsonConvert.SerializeObject(payload);
 
        using (WebClient client = new WebClient())
        {
            NameValueCollection data = new NameValueCollection();
            data["payload"] = payloadJson;
 
            var response = client.UploadValues(_uri, "POST", data);
 
            //The response text is usually "ok"
            return _encoding.GetString(response);
        }
    }

    public class Payload
    {
        [JsonProperty("username")]
        public string Username { get; set; }

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

With these files in place, we can update the code of the function in run.csx file. First, add some usings and other code needed for the rest of the function:

#r "Newtonsoft.Json"
#load "slackClient.csx"

using Kentico.Kontent.Delivery;
using System.Net;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
using System.Security.Cryptography;

static bool blnValid = false;
static bool blnPublish = false;
static string slackChannel = "https://hooks.slack.com/services/[...]";
static string previewKey = "";
static string projectId = "";
static string webhookSecret = "";

private static string GenerateHash(string message, string secret)
{
    secret = secret ?? "";
    UTF8Encoding SafeUTF8 = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
    byte[] keyBytes = SafeUTF8.GetBytes(secret);
    byte[] messageBytes = SafeUTF8.GetBytes(message);
    using (HMACSHA256 hmacsha256 = new HMACSHA256(keyBytes))
    {
        byte[] hashmessage = hmacsha256.ComputeHash(messageBytes);
        return Convert.ToBase64String(hashmessage);
    }
}

Fill in the slackChannel variable with the URL from a previous section. Add the webhook secret from earlier, then the Project ID and Preview API Key from Kontent’s Project settings > API keys page.

Now for the main code! In the Run method, we first need to get the body of the webhook and validate the signature:

// Get the signature for validation
IEnumerable<string> headerValues = req.Headers["X-KC-Signature"];
var sig = headerValues.FirstOrDefault();
 
// Get body
string content;
using (Stream receiveStream = req.Body)
{
	using (StreamReader readStream = new StreamReader(receiveStream, Encoding.UTF8))
	{
		content = readStream.ReadToEnd();
	}
}

// Generate a hash using the content and the webhook secret
var hash = GenerateHash(content, webhookSecret);
 
// Verify the notification is valid
if(sig != hash)
{
	return new HttpResponseMessage(HttpStatusCode.Unauthorized) {
		ReasonPhrase = "Signature validation failed"
	} as IActionResult;
}

If the signature is valid, we can get parse the body with JSON and verify it’s a publish operation. The webhook notification is really only a notification and does not contain the actual item data, just the codenames. Therefore, we will call a custom PostToSlack method that will load the data by the content items’ codenames:

var settings = new JsonSerializerSettings
	{
		NullValueHandling = NullValueHandling.Ignore,
		MissingMemberHandling = MissingMemberHandling.Ignore
	};
dynamic data = JsonConvert.DeserializeObject(content, settings);

if (data == null)
{
	return new HttpResponseMessage(HttpStatusCode.BadRequest) {
		ReasonPhrase = "Please pass data properties in the input object"
	} as IActionResult;
}

// Make sure it's a valid operation
if(data.message.operation.ToString().ToLower() == "publish")
{
	List<string> lstCodeNames = new List<string>();

	foreach(var item in data.data.items)
	{
		lstCodeNames.Add(item.codename.ToString());
	}
	if(lstCodeNames.Count > 0)
	{
		await PostToSlack(lstCodeNames, log);
	}
	return new HttpResponseMessage(HttpStatusCode.OK) as IActionResult;
}

return new HttpResponseMessage(HttpStatusCode.NoContent) as IActionResult;

Finally, we need to create our PostToSlack method. This will use our SlackClient and loop through the codenames, posting a message to Slack for each updated content item:

private static async Task PostToSlack(List<string> list, ILogger log)
{
    try
    {
        SlackClient slackclient = new SlackClient(slackChannel);
        IDeliveryClient deliveryclient = DeliveryClientBuilder
            .WithOptions(builder => builder
                .WithProjectId(projectId)
                .UsePreviewApi(previewKey)
            .Build())
            .Build();

        foreach(string codename in list)
        {
            DeliveryItemResponse response = await deliveryclient.GetItemAsync(codename);
            if(response != null)
            {
                var item = response.Item;    
 
                var msg = slackclient.PostMessage(
                    username: "KontentBot",
                    text: "Content update: '" + item.System.Name
                );
            }
            else
            {
                log.LogInformation($"Item {codename} not found!");
            }
        }
    }
    catch (Exception ex)
    {
        log.LogInformation(ex.Message.ToString());
    }
}

The Slack message will be bearing “Content update: {item name}” here. Feel free to adjust that based on your actual content type and output more valuable information.

Testing

With everything in place, we’re ready to test. Open Kentico Kontent and publish a content item:

Then, verify that the message was posted to the Slack channel:

Great job! Now it’s easier for your team to stay updated on what’s happening.

Moving Forward

In this article, I showed you how you can integrate your company’s Slack channel into your content management process. This functionality can help your team be more productive by delivering updates in real time and let you know of issues sooner. I hope it helps you and your team get over this home office period smoothly. Find the full source code here:

Get the code

Written by
Eric Dugre

I am a Support Platform Specialist who joined Kentico in 2015. When I’m not helping customers, I focus on developing open-source projects, integrations, and SDKs for Kentico products.

More articles from Eric

Subscribe to Kentico Kontent Newsletter

Stay in the loop. Get the hottest updates while they’re fresh!