Do-it-Yourself Templating for Slack API (Go)

Slack, like any service with a bit of history, has quirks in its API. One such quirk that confused me was the parse: true flag and the use of pseudo-Markdown that they accept in the chat.postMessage API. I had a fun time digging around this feature and achieve what I wanted to do.

So first of all. I’m not entirely sure that parse: true in the message payload does, but one of the things that it certainly do is to make @username into actual links. But it also stops the Markdown from being interpreted. For example, <https://foo.com|foo> style links and *foo* _bar_ don’t turn into a link, bold font, or italic font, respectively.

But… But… You want to mix them, don’t you? I did.

I also wanted to use templates to craft the messages. I wanted to create reminder messages where the days left until the deadline (among many other things) would change depending on when the message was fired, something like:

Hey @username!This is a friendly reminder that you only have {{ .DaysLeft }} days left until the deadline!Make sure to *<https://link.to/some/form|submit your paper>*!

This is a simple piece of Go’s text/template source. I wanted to generate a message using the above template. You can see that I wanted to use a static @username but also include a bold link to a website.

After various failed attempts trying to mix parse: true/false , as_user: true/fase , and many other incantations, I realized that the traditional payload just wasn’t going to cut it. Whatever I did, I kept getting only one of those markups to render correctly.

This is when I realized what the Block kit was for. I had used the Block kit before, and I knew that they could produce many interesting message types, but until this point I only thought that they were nifty tools to create interactive messages.

But it finally dawned on me that with Block kit you can mix different text sections. For example, the above template could be re-written as:

{
"blocks": [
{
"type": "section",
"text": {
"type": "plain_text",
"text": "Hey <@username>!"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "This is a friendly reminder that you only have {{ .DaysLeft }} days left until the deadline!\n\nMake sure to *<https://link.to/some/form|submit your paper>*!"
}
}
]
}

And you can first process the template rendering with text/template then use encoding/json to unmarshal this into an appropriate struct , then feed it to your favorite Slack API binding. This time I happened to be using https://github.com/slack-go/slack so you would do something like:

//go:embed templates/*
var content embed.FS
// error handling omitted for brevitydata, _ := content.ReadFile(filename)
tmpl, _ := template.New("tmpl").Parse(string(data))
var buf bytes.Buffer
_ := tmpl.Execute(&buf, arg) // pretend arg has everything required
// Note: as of this writing (Oct 2021), I couldn't unmarshal
// Block kit to slack.Blocks struct, so I had to devise a hack (*1)
var blocks slack.Blocks
_ := json.Unmarshal(buf.Bytes(), &blocks)
var options []slack.MsgOption
options = append(options, slack.MsgOptionBlocks(blocks.Blocks...))
_, _, _ := client.PostMessage(channel, options...)

Now this was almost good enough, but I had one remaining itch to scratch. Isn’t JSON just horrendous to edit? Especially if you have a piece of text that spans over multiple lines?

So I changed the source to be YAML. I know, YAML has its problems, and I’m doing extra processing. But in this circumstance, I judged the side effects to be minimal. Now the source template read:

blocks:
- type: section
text:
type: plain_text
text: "Hey <@username>!"
- type: section
text:
type: mrkdwn
text: |
This is a friendly reminder that you only have {{ .DaysLeft }} days left until the deadline!

Make sure to *<https://link.to/some/form|submit your paper>*!

And there was much rejoicing. Hope this helps somebody who wants to do something similar in Go.

(*1) So for whatever reason I couldn’t unmarshal blocks with two ore more entries in it, so I wrote this:

type Blocks struct {
Blocks []slack.Block `json:"blocks"`
}
type blockhint struct {
Typ string `json:"type"`
}
func (b *Blocks) UnmarshalJSON(data []byte) error {
var proxy struct {
Blocks []json.RawMessage `json:"blocks"`
}
if err := json.Unmarshal(data, &proxy); err != nil {
return fmt.Errorf(`failed to unmarshal blocks array: %w`, err)
}
for _, rawBlock := range proxy.Blocks {
var hint blockhint
if err := json.Unmarshal(rawBlock, &hint); err != nil {
return fmt.Errorf(`failed to unmarshal next block for hint: %w`, err)
}
var block slack.Block
switch hint.Typ {
case "actions":
block = &slack.ActionBlock{}
case "context":
block = &slack.ContextBlock{}
case "divider":
block = &slack.DividerBlock{}
case "file":
block = &slack.FileBlock{}
case "header":
block = &slack.HeaderBlock{}
case "image":
block = &slack.ImageBlock{}
case "input":
block = &slack.InputBlock{}
case "section":
block = &slack.SectionBlock{}
default:
block = &slack.UnknownBlock{}
}
if err := json.Unmarshal(rawBlock, block); err != nil {
return fmt.Errorf(`failed to unmarshal next block: %w`, err)
}
b.Blocks = append(b.Blocks, block)
}
return nil
}

And used this in place of slack.Blocks :

var blocks mypkg.Blocks

Go/perl hacker; author of peco; works @ Mercari; ex-mastermind of builderscon; Proud father of three boys;