Using Webhooks in Umbraco 13 to send messages to Slack

Posted written by Paul Seal on December 19, 2023 Modern .NET Umbraco

Umbraco 13 was released last week and one of my favourite features that got introduced was Webhooks.

What they make possible is to fire off a POST request to an end point when a particular event is fired, such as the publish event.

You get to choose where you want the data to be sent off to, which event you would like to hook into and which content type(s) you would like this to be restricted to, if any.

They post some data about the entity which the event relates to, so in the case of a page being published, it would be details about the page and its properties.

These are called outgoing webhooks.

I've worked with incoming webhooks before, when I created a NuGet package called SlackBotMessages which allows you to post messages to Slack. It's been quite a successful NuGet package to be fair with almost 1 million downloads on NuGet.

So when I saw Umbraco had created outgoing webhooks I decided to combine them with the Slack incoming webhooks to send messages.

To set up an incoming webhook in Slack, follow steps 1, 2 and 3 of these instructions on the Slack website.

At first I tried just creating an Umbraco webhook and pointing it to my Slack webhook URL

1

Umbraco webhook calling Slack incoming webhook directly

As you might expect, that didn't work because the data which was posted from Umbraco to the Slack webhook was not in the right format for Slack to understand. 

I soon realised I would need to find out what format the data was in first, manipulate it and then post it to Slack.

To find out what data was being posted, I used a free tool called webhook.site which was recommended by Lotte in the recent Umbraco 13 unboxing video.

When you go to webhook.site you are given a unique webhook URL.

2

My unique webhook URL

So I took that and used it in my Umbraco webhook as the Url property

3

Using the webhook.site URL

With that in place, when I save and published the home page, it fired off the data to the webhook and on the webhook.site page I saw the data come through like this:

4

Data posted to the webhook

I thought a good idea would be to create an API Controller that would receive the data in this format, transform it to the correct format for slack messages and then send it off to the Slack webhook.

It turns out this wasn't going to be the optimal solution, but keep reading and at the end you will see the best solution for all this in the Update.

Now that I knew how the data looked I created a record to read the json data into an object.

Here is the code for the record:

namespace MyProject.Models;

public record WebhookEntity(string Name, DateTime CreateDate, 
    DateTime UpdateDate, Route Route, string Id, string ContentType);

public record Route(string Path, Startitem StartItem);

public record Startitem(string Id, string Path);

WebhookEntity

I installed the package I mentioned earlier, SlackBotMessages using NuGet:

dotnet add package SlackBotMessages --version 2.1.0

Add the SlackBotMessages package

I then created a simple api controller which receives the posted data, converts it to the WebhookEntity record object and then uses the data from it to send a message to Slack using the SlackBotMessages NuGet package.

Here is the code for the Api Controller:

using Microsoft.AspNetCore.Mvc;
using SlackBotMessages.Models;
using SlackBotMessages;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Web.Common.Controllers;
using Umbraco.Cms.Core.Models.ContentEditing;
using MyProject.Models;

namespace MyProject.Controllers;

public class SlackController : UmbracoApiController
{
    [HttpPost]
    public async Task<ActionResult> SendMessage([FromBody]WebhookEntity entity)
    {
        var client = new SbmClient("https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX");
        
        var message = new Message
        {
            Text = $"{entity.Name} was published"
        };

        message.AddAttachment(new Attachment()
            .AddField("Name", entity.Name, true)
            .AddField("Path", entity.Route.Path, true)
            .AddField("CreateDate", entity.CreateDate.ToString("dd/MM/yyyy hh:mm:ss"), true)
            .AddField("UpdateDate", entity.UpdateDate.ToString("dd/MM/yyyy hh:mm:ss"), true)
        );

        await client.SendAsync(message);

        return Ok(entity.Id);
    }
}

Slack Api Controller

Now all I needed to do was put in the URL of the Api Controller into the URL property of my Umbraco webhook.

5

Using the Api Controller endpoint in the webhook URL

With the code in place it now sends a message to Slack every time someone publishes the home page.

Obviously we can remove the restriction on it just being the home page and we can get a message every time any page is published.

We could also have a different api method per event type, e.g. publish, delete etc.

6

Message sent to Slack

I hope this article has given you some food for thought, let me know what you think to these webhooks and what ideas you have for use cases.

Update

My friend Nik pointed out that I didn't need to use an API Controller at all. I could just create my own webhook event in which I could transform the body of the data that gets posted to the webhook. He gave me this link to follow and sure enough it works a treat.

First of all I created my own custom webhook event:

using Microsoft.Extensions.Options;
using SlackBotMessages.Models;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Notifications;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Sync;
using Umbraco.Cms.Core.Webhooks;
using Constants = Umbraco.Cms.Core.Constants;

namespace MyProject.WebhookEvents;

[WebhookEvent("Content Published for Slack", Constants.WebhookEvents.Types.Content )]
public class ContentPublishedSlackEvent : WebhookEventContentBase<ContentPublishedNotification, IContent>
{
    private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;

    public ContentPublishedSlackEvent(IWebhookFiringService webhookFiringService, IWebhookService webhookService, 
        IOptionsMonitor<WebhookSettings> webhookSettings, IServerRoleAccessor serverRoleAccessor, 
        IPublishedSnapshotAccessor publishedSnapshotAccessor)
        : base(webhookFiringService, webhookService, webhookSettings, serverRoleAccessor)
    {
        _publishedSnapshotAccessor = publishedSnapshotAccessor;
    }

    public override string Alias => "ContentPublishedSlack";

    protected override object? ConvertEntityToRequestPayload(IContent entity)
    {
        if (_publishedSnapshotAccessor.TryGetPublishedSnapshot(out IPublishedSnapshot? publishedSnapshot) is false 
            || publishedSnapshot!.Content is null)
        {
            return null;
        }

        IPublishedContent? publishedContent = publishedSnapshot.Content.GetById(entity.Key);

        if (publishedContent is null) return null; 

        var message = new Message
        {
            Text = $"{entity.Name} was published"
        };

        message.AddAttachment(new Attachment()
            .AddField("Name", entity.Name, true)
            .AddField("Url", publishedContent.Url(), true)
            .AddField("Update Date", entity.UpdateDate.ToString("dd/MM/yyyy hh:mm:ss"), true)
            .AddField("Published By", publishedContent.WriterName() ?? "Unknown", true)
        );

        return message;
    }


    protected override IEnumerable<IContent> GetEntitiesFromNotification(ContentPublishedNotification notification) 
        => notification.PublishedEntities;
}

Custom webhook event code

Then I registered the custom webhook event

using MyProject.WebhookEvents;
using Umbraco.Cms.Core.Composing;

namespace MyProject.Composers;

public class CustomWebhookComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.WebhookEvents().Add<ContentPublishedSlackEvent>();
    }
}

Custom webhook composer

And with that in place I can change my Umbraco webhook to point directly to the slack webhook URL and pick the new Content Published for Slack event.

8

Umbraco webhook using the new event

And that sends the message directly to slack without us needing to have an API Controller at all.

I think this is probably the best way of doing it, but at least we have found 2 ways to do it.

This is one of the great benefits of blogging and sharing what you are working on, you benefit from other people's feedback.

7

What the message looks like in Slack