Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Block kit message templating #476

Open
kneal opened this issue Feb 17, 2019 · 29 comments

Comments

@kneal
Copy link

commented Feb 17, 2019

Hey,

I was wondering if anyone had time to look into the new Slack block kit style messaging. I think it would be a great addition to the library.

https://api.slack.com/block-kit

@MattDavisRV

This comment has been minimized.

Copy link
Contributor

commented Feb 21, 2019

I'm working on an initial implementation of blocks. Hope to have a PR up in the next few days

@MattDavisRV

This comment has been minimized.

Copy link
Contributor

commented Feb 23, 2019

I've opened PR #480 to as an initial implementation of Block Kit

@MattDavisRV

This comment has been minimized.

Copy link
Contributor

commented Feb 27, 2019

The PR has been merged into master. Please take a look at the examples and read me file, it can be very verbose at times, but allows for flexibility when creating blocks.

@darkliquid

This comment has been minimized.

Copy link

commented Feb 27, 2019

I was looking at this and it's not clear how you actually send messages in this new block format. Constructing the actual block Message object is easy enough, but what do you do with that afterwards to actually send it?

@MattDavisRV

This comment has been minimized.

Copy link
Contributor

commented Feb 27, 2019

I'm responding to the slack request (action, slash command, etc) with the json generated, the entire Message object (as json)

@esselius

This comment has been minimized.

Copy link

commented Mar 8, 2019

@MattDavisRV Could you provide an example of this please? 😄

@MattDavisRV

This comment has been minimized.

Copy link
Contributor

commented Mar 8, 2019

Here's an example of a slack command being initiated by the user, with the word "help" entered after the command.

I use chi to handle my requests, this example is setup with that in mind.

`
func RequestHandler() http.HandlerFunc {

return func(w http.ResponseWriter, r *http.Request) {

	// Create an empty response
	var response slack.Message

	// All responses will be of type json
	w.Header().Set("Content-Type", "application/json")

	// Parse the request from slack
	slackRequest, _ := slack.SlashCommandParse(r)

	// Check for the word help
	if slackRequest.Text == "help" {

		// Shared Section, this is like an <hr /> tag in html
		divSection := slack.NewDividerBlock()

		// Create text element to display to the user
		headerText := slack.NewTextBlockObject(
			"mrkdwn",
			"*Need some help?*\n Here are different things you can do!",
			false,
			false,
		)

		// Add a close button to the right of the text
		closeBtn := slack.NewTextBlockObject("plain_text", "Close", false, false)
		closeBtnEle := slack.NewButtonBlockElement("close", "close", closeBtn)

		// Create block with both text and close button
		headerSection := slack.NewSectionBlock(headerText, nil, closeBtnEle)

		// Add block to response message.  If you had additional blocks, you'd pass them in here
		response = slack.NewBlockMessage(headerSection)

	}

	// Note: if the word "help" was not passed in during this example, the response
	// will be empty, likely resulting a validation error from slack.  Your application should
	// have additional logic, this is just a simple example.

	// Normal JSON stuff
	responseBytes, _ := json.Marshal(response)
	w.Write(responseBytes)
	return

}

}
`

@esselius

This comment has been minimized.

Copy link

commented Mar 8, 2019

@MattDavisRV Excellent, thanks!

Do you also have any pointers regarding how to handle button action requests nicely?

@MattDavisRV

This comment has been minimized.

Copy link
Contributor

commented Mar 8, 2019

In my use case, I've built an app to look up employees within our company. I've added a url to slack such as: http://my-url.com/slack-app/action to handle interactive requests. When a request is made to this URL, I have the following code. Currently, this only supports the close button and viewing an employee. If I were to add additional actions, I'd likely put them in a switch statement. See comments in code below.

There are probably better ways to handle errors and responses using this package that I have not implemented, I would recommend using those in your project.

I've added additional comments in the code for the sake of this comment.

`func (s *Service) ActionHandler() http.HandlerFunc {

return func(w http.ResponseWriter, r *http.Request) {

	// Parse input from request
	if err := r.ParseForm(); err != nil {
                     // getSlackError is a helper to quickly render errors back to slack
		responseBytes := getSlackError("Server Error", "An unknown error occurred")
		w.Write(responseBytes)
		return
	}

	interactionRequest := slack.InteractionCallback{}
	json.Unmarshal([]byte(r.PostForm.Get("payload")), &interactionRequest)

            // Get the action from the request, it'll always be the first one provided in my case
	actionValue := interactionRequest.ActionCallback.Actions[0].Value

            // Handle close action
	if strings.Contains(actionValue, "close") {

                     // Found this on stack overflow, unsure if this exists in the package
		closeStr := `{
			'response_type': 'ephemeral',
			'text': '',
			'replace_original': true,
			'delete_original': true
		}`

                     // Post close json back to response URL to close the message
		http.Post(interactionRequest.ResponseURL, "application/json", bytes.NewBuffer([]byte(closeStr)))
		return

	}

	// Only implemented view at this point, error everything else
	if !strings.Contains(actionValue, "view_") {

		responseBytes := getSlackError("Server Error", "An unknown error occurred")
		w.Write(responseBytes)
		return
	}

             // In this example, the action will be "view_12345", with 12345 being a unique id for each employee.  In a larger application, I'd likely grab the word before the _ and switch on that value to render the correct view to the user. 

	eid := strings.Replace(actionValue, "view_", "", -1)

             // This function will generate blocks, similar to the previous comment.  responseData is an instance of slack.Message
	responseData, err := s.lookupByEmployeeID(eid)

	// Handle any errors from the lookup
	if err != nil {
		responseBytes := getSlackError("Employee Search", "Unable to read request from slack, please try again.")
		w.Write(responseBytes)
		return
	}

	// Replace original text that included in the button they clicked.  This is optional, but a better end user experience in my opinion.
	responseData.ReplaceOriginal = true

	// Write results back to to slack.
	responseBytes, _ := json.Marshal(responseData)

	http.Post(interactionRequest.ResponseURL, "application/json", bytes.NewBuffer(responseBytes))

	return

}

}

// Helper function to display errors to the user only and not to the channel
func getSlackError(system, msg string) []byte {

respoonse := slack.Message{
	Msg: slack.Msg{
		ResponseType: "ephemeral",
		Text:         fmt.Sprintf("%s: %s", system, msg),
	},
}

responseBytes, _ := json.Marshal(respoonse)

return responseBytes

}

`

@khayyamsaleem

This comment has been minimized.

Copy link

commented Mar 8, 2019

is it possible to use the PostMessage function to send BlockKit style messages?

@khayyamsaleem

This comment has been minimized.

Copy link

commented Mar 8, 2019

ahh okay. so my guess is that something like the following:

// MsgOptionBlocks provide blocks for the message.
func MsgOptionBlocks(blocks ...Block) MsgOption {
	return func(config *sendConfig) error {
		if blocks == nil {
			return nil
		}

		blocks, err := json.Marshal(blocks)
		if err == nil {
			config.values.Set("blocks", string(blocks))
		}
		return err
	}
}

would need to be added to chat.go.
That way, it could be added to the trailing params for PostMessage. New to go and this repo so I'm unsure, but can anyone confirm if that is what it would take to get PostMessage to support blocks?

@khayyamsaleem

This comment has been minimized.

Copy link

commented Mar 9, 2019

@MattDavisRV since the Block struct was made private, and the only exposed form is through a function that wraps it in an Msg, is there a different way to add this to chat.go? Would it be a bad idea to make block an exported type again?

@mikegleasonjr

This comment has been minimized.

Copy link

commented Mar 11, 2019

👍 for @khayyamsaleem request.

Adding blocks support only to be able to reply to slack requests is kind of half-baked. Sorry it is merged to master so to me the owners is considering this prod ready for a future release.

Slack encourages to use blocks to send regular messages over attachments:

This feature is a legacy part of messaging functionality for Slack apps. We recommend you stick with layout blocks as above, but if you still want to use attachments, read our caveats below.

@darkliquid

This comment has been minimized.

Copy link

commented Mar 11, 2019

As a temporary thing that shouldn't require altering the library itself, I knocked together this (currently untested) snippet, which you can put in your own app as a helper:

// MsgOptionBlocks applies the blocks from a block message to an existing message.
func MsgOptionBlocks(msg slack.Msg) slack.MsgOption {
	return slack.MsgOptionCompose(
		// Copy the blocks from the source message into the new message
		slack.UnsafeMsgOptionEndpoint("", func(v url.Values) {
			blocks, err := json.Marshal(msg.Blocks)
			if err == nil {
				v.Set("blocks", string(blocks))
			}
		}),
		// Since the unsafe option above erases the endpoint, reset it to
		// the normal post one.
		slack.MsgOptionPost(),
	)
}

this allows you to use the exported NewBlockMessage method to create a block message, then pass it into the helper to have the blocks defined on it applied to the message you want to send via PostMessage.

Hacky? Yes. But in lieu of a change to the library to export blocks and make a native MsgOptionBlocks method, this may suffice.

@esselius

This comment has been minimized.

Copy link

commented Mar 11, 2019

@MattDavisRV Thanks! Very helpful!

@MattDavisRV

This comment has been minimized.

Copy link
Contributor

commented Mar 11, 2019

@mihaip I agree it should also have a way to respond to the original request. I was going to submit a PR for a way to do that, just haven't had time. The original reason I did not do that is that I've run into edge cases of responding back to slack within the timeframe (3 seconds? Don't remember off the top of my head), so I general respond via the response url instead to account for unknown variables when processing requests.

I'll try to get a PR in this week with something similar to what's been posted in this thread.

@Multiply

This comment has been minimized.

Copy link

commented Mar 14, 2019

Is there a reason the block type is unexported?

I'm having a hard time making a block message, with a variable length of blocks.

@MattDavisRV

This comment has been minimized.

Copy link
Contributor

commented Mar 14, 2019

It wasn't in the original PR, but was requested by @james-lawrence to be made private before it was merged. I'll add another function in my upcoming PR to append blocks after they've been initialized.

@Multiply

This comment has been minimized.

Copy link

commented Mar 14, 2019

I'm currently using it like

msg := slack.NewBlockMessage()
msg = slack.AddBlockMessage(msg, ...)

But that would result in a lot of copy of the same message over and over, I fear.

@james-lawrence

This comment has been minimized.

Copy link
Collaborator

commented Mar 14, 2019

@Multiply the block type is an interface there isn't a need to export it. as for it being merged into master....

particularly new APIs from slack may be merged in without being fully baked from a global perspective. Generally my assumptions for PRs is that it fullfills some need the library failed to fill.

I then evaluate the PR around the following:

  1. does the functionality apply well globally? i.e. is it useful for everyone or is it only really applicable to their application. for example: caching results, generally caching tends to be application specific so I won't consider those in the library core.
  2. is the code maintainable? (is it using the internal library utilities correctly?)
  3. does it expand the libraries api in unacceptable ways? (block interface for example didn't need to be public to do its job)
@khayyamsaleem

This comment has been minimized.

Copy link

commented Mar 19, 2019

@alyosha

This comment has been minimized.

Copy link
Contributor

commented Mar 20, 2019

Sorry I did not see this issue before opening #490, would appreciate any/all input

@khayyamsaleem

This comment has been minimized.

Copy link

commented Mar 21, 2019

is this good to close?

@alyosha

This comment has been minimized.

Copy link
Contributor

commented Mar 22, 2019

@khayyamsaleem there is a separate but related issue I am currently working on a PR for regarding the unmarshalling of JSON responses from the block interaction callback. We could open a new issue but might be easier to keep tracking here

@ArcticSnowman

This comment has been minimized.

Copy link

commented Mar 22, 2019

One use case that might need the Block template is when you want to return a list of results, like a search, and you want to create a TextBlock per item. Maybe with SeparatorBlocks between.. Then being able to have a []Block to fill makes life easier.

Now if you want to keep the low level interface private, then the MsgOptionBlocks needs to be accumulative. So that you can pass in more that one to the [Send|Post]Message call and it correctly assembles all the block.

@alyosha

This comment has been minimized.

Copy link
Contributor

commented Mar 22, 2019

have a working branch for dynamically unmarshalling the JSON responses for actions/blocks since both can contain multiple, unique types in the response from Slack (talking about InteractionCallback specifically)

not super confident in implementation so will clean up, share, and ask for feedback 🙏

@alyosha

This comment has been minimized.

Copy link
Contributor

commented Mar 24, 2019

will try to push in the next day or so, I'm pretty much done and just fixing/adding tests at the moment. the functionality is really limited without the ability to unmarshal the callback from block interactions, so I'm trying to get this done as soon as I can.

@alyosha

This comment has been minimized.

Copy link
Contributor

commented Mar 26, 2019

@khayyamsaleem got a PR up, though it's still WIP
#494

examples are failing since the branch I'm working off of is from my forked repo, but should resolve that in the next day or so then I will add tests. I have confirmed the behavior pretty extensively around InteractionCallbacks (still not exhaustive), but have yet to confirm channel history.

Would love feedback from any/everyone on this one--I'm sure there is a more elegant way to do the same thing 🙏

@WtfJoke

This comment has been minimized.

Copy link
Contributor

commented Aug 7, 2019

examples are failing since the branch I'm working off of is from my forked repo

Is that still the same cause? Currently examples are broken in latest master, see #564

EDIT: Created PR #567

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.