So my day off yesterday didn't go quite according to plan. The original thought was that I'd spend the day trying out a new service to see if it more closely fit what I needed for my new idea. And I was happy with that thought, the evening came the night before and it was still the plan...until I gave my social media one last check.

What happened when I checked social media? Well I saw that Slack had announced that for those clients for whom external connections (like public APIs for slack apps) were difficult - there was now an alternative: Socket Mode. This new mode was a toggle at the application level and enabled the ability for your application to make a WebSocket connection and for that connection to be the way communication was handled.

I immediately saw benefit with this, especially for internal apps helping tech teams with areas like builds - as you often want environments to be as tightly controlled as possible. But as ever, there's native support for a lot of languages - not .NET

So instead of my planned day off, I spent the day updating my [Slack.NetStandard] NuGet package to support Socket Mode, and writing a WebSocket console app to give it a try.

Slack Setup

This is a really nice and easy section because Socket Mode has been made as a pretty straightforward toggle. It is an either/or situation - so if you enable socket mode you will stop receiving your HTTP requests in your app, but the settings are stored so if you toggle it off you don't have to set everything up again.

So to toggle your app on and off there is a socket mode option in your app menu

A screenshot showing theslack app socket mode toggle

The other thing you need to set up is an app token. Whereas most of the tokens you deal with in a Slack app are specific to a workspace installation (and still are, socket mode doesn't change the way you interact with the web api for each installation), an app token allows you to perform specific actions where the need is application wide. One of these actions is the new app.connections.open command required to get a WebSocket url. So you need to go to your basic app settings and set one up.

A screenshot showing the slack app token options

And that's it from the Slack side. App installation and everything else still needs to occur as normal - but you don't need to verify the requests coming in, you won't need the signing secret, to receive the requests the app token is the key.

Code Setup

Now that you have your slack app socket-ready, you can wire it up into an application. I used a console app as I wanted to be able to test fast inside my package environment, but you could equally use an ASP.Net Core background service or equivalent - the whole point of changing this structure is it allows you to fit this process into your environment the way that works for you. Because of this I'm going to focus on C# code snippets rather than the full setup.

Before you worry about sockets - you need a URL to connect to. This is where the app token comes in as you need to make a call to the API to get that. This is available as a method in the SlackWebApiClient

var webapi = new SlackWebApiClient(apptoken);
var url = await webapi.Apps.OpenConnection();
var connectionUri = new Uri(url.Url, UriKind.Absolute);

Not too bad - we have the URL. So now we need a client to use it on.

ClientWebSocket

WebSockets are a two way means of communication. I'm using the standard ClientWebSocket available in the .NET Framework, so your implementation may be different, but for this we connect to the URL

var client = new ClientWebSocket();
client.ConnectAsync(new Uri(url.Url, UriKind.Absolute), default);

At this point we can handle both comms in and out with two async methods - ReceiveAsync and SendAsync

The first thing that happens after you've connected is a bit of a handshake. Slack will send you a "hello" message. It contains useful information such as how long your connection is expected to last (you can and will be expected to reconnect with a different url - we'll touch on how that's communicated in a bit) and you have to acknowledge it by sending a response. There's no documentation but it appears to be any response will do - I believe it's just to confirm the two way connection is set up before Slack sends you more information. So let's wire that up.

Receiving the hello message

Okay so the hello message is going to be the same as any other message - you're just receiving information. Depending on your buffers and size of the payload, this might be sent over multiple messages so you need to keep reading the messages until one of them is tagged as being the end of the message. That's your JSON payload.

I've used a StringBuilder to grab the text then I convert it at the end, this is fast and easy for me to test, but there's lots of ways you could do this. I've just connected so I know I'm looking for a Hello object.

var osb = new StringBuilder();
var memory = new Memory<byte>(new byte[2048]);

var messageEnd = false;
while (!messageEnd)
{
    var result = await client.ReceiveAsync(memory, cancel.Token);

    if (result.MessageType == WebSocketMessageType.Close)
    {
        cancel.Cancel();
        Reconnect = false;
        return;
    }

    osb.Append(Encoding.UTF8.GetString(memory.Span));
    messageEnd = result.EndOfMessage;
}
//... other logic no doubt
var hello = JsonConvert.DeserializeObject<Hello>(osb.ToString());
osb.Clear();

You'll notice I've got code that checks the MessageType. This loop is how I'm checking all my messages moving forward - so although I've just connected, issues could happen at any time and if the Client tells me that the connection has been closed I need to get out of my loop (probably log a partial message if one exists) and reconnect.

So we have the hello message. Personally I don't much mind about the details so I'm just going to respond and then we can worry about the main loop. I'll just send simple text.

await SendMessage(client, "hello");

private static Task SendMessage(ClientWebSocket client, string text)
{
    return client.SendAsync(
        Encoding.UTF8.GetBytes(text),
        WebSocketMessageType.Text, true, default);
}

Regular loop

Now with both these async methods going on, you need to ensure neither are causing blocks to occur, otherwise your Slack app will start getting errors. Once this initial handshake has occurred you'll start getting your regular messages from Slack. The ReceiveAsync loop above will keep going round and round picking up messages, and each message will need an acknowledgement through the SendAsync method. The main difference is how they're structured.

Envelopes and Acknowledgements

Traditionally we receive Events, Commands and InteractionPayloads as distinct json objects with their own wrappers and endpoints. And as you process those requests you return an HTTP status with some optional text.

With Socket Mode you still have those underlying payloads - and Slack.NetStandard worries about Deserializing those for you - but now every request (with the exception of Hello and Disconnect) is wrapped in an Envelope which behind the scenes looks like this.

{
  "payload": {},
  "envelope_id": "xxx",
  "type": "xxx",
  "accepts_response_payload": true
}

Slack.NetStandard, when asked to return an Envelope, examines the type of envelope request and deserializes the payload appropriately. The envelope type is different for each ("slash_commands", "interactive", "events_api") or you can check the type - so you can keep your logic mostly the same (the latest version of Slack.NetStandard.Endpoint has an extension to convert from Envelope to SlackInformation so almost all the logic from that or RequestHandler should require minimal changes too)

var env = JsonConvert.DeserializeObject<Envelope>(msg);
if (env.Payload is SlashCommand)
{
    // ...
}

As with all Slack apps you have to respond within a few seconds to ensure that Slack knows you've received the message and doesn't display an error to your user. In Socket Mode this is done through an Acknowledgement object. The Acknowledgement object needs the Envelope ID and the payload to go with the response if required.

var ack = new Acknowledge
{
    EnvelopeId = env.EnvelopeId,
    Payload = new Message
    {
        Blocks = new List<IMessageBlock>
        {
            new Section("This is from a socket....shh!")
        }
    }
}

await SendMessage(client, JsonConvert.SerializeObject(ack));

And this is your Socket Mode Slack app - over and over again your app is picking up envelopes and sending back Acknowledgements. There is one small exception to this (other than Hello) which is worthy of note.

Please Disconnect

Every now and again your Socket Mode app will be asked to disconnect. This could happen at any time, and is something you have to handle - you have to close your connection and pick up a new one. A little annoyingly Slack doesn't do this via an envelope - but via a "disconnect" message (As there's no envelope I perform a simple check to see if "envelope_id" is in the text before I deserialize). Slack sends a reason for the request so it's worth getting the detail for your logs. And again Slack.NetStandard supports it

var hello = JsonConvert.DeserializeObject<Disconnect>(osb.ToString());

This is worth noting as it means you have to write reconnection code in your app to ensure continuity. Personally I used .NET Channels to entirely separate my Socket work from my processing of the events while keeping it all in one line. Had to mention it though - last thing I wanted was to mention the messsage loop and not the connection loop that has to be wrapped in!

So what do I think of Socket Mode?

My day job involves working in a highly regulated industry where security is paramount and although I adore working in Slack Apps, that open API for Slack did mean we had to restrict what we handled through it. Personally speaking I think this is a great way to allow a much more robust use of Slack for internal process and communication. Although the Console app was a fun starter and in less than 200 lines of code I had something that let me test and fix issues with a real app - personally speaking I think that where it's required I'll be using ASP.Net with some Background Services.

As positive a step as I think it is, I think it's also worth mentioning that this is for specific use cases. On my day to day apps I definitely feel the maintainance, complexity and cost of a serverless app with an API Gateway is the way to go - it's great to have options, but don't chang your model unless you really need to.