Repository: slack-go/slack (go)

Issue:
Please Add parse function for InteractionCallback
Now, you have to use `(*http.Request).FormValue("payload")` to get payload string for `slack.InteractionCallback` as follows.

https://github.com/nlopes/slack/blob/8fe460f3467836471f96e0bdfc9689de165f1ee5/examples/buttons/buttons.go#L45-L46

This is a bit confusing and may cause users to change their code when slack API changes.

So, could you add parse function for InteractionCallback `func(*http.Request) (InteractionCallback, error)` like `SlashCommandParse`?

https://github.com/nlopes/slack/blob/0afac90b917f77d7c59d638420d5d40fe65eee61/slash.go#L25

Pull request description:
Add interaction callback parser
Resolves #660.

### Adds
- `InteractionCallbackParse` function
- Adds a test for the added function

For the sake of consistency, I took inspiration from the argument to [`SlashCommandParse`](https://github.com/snowboardit/slack/blob/28ba0ac9e25fd759fbc95530998cc678218622c3/slash.go#L29-L51) and implemented `InteractionCallbackParse` in a similar fashion. I believe this to be the _most_ idiomatic argument, since we are using the std lib, but I'm relatively new to Go so please let me know if there is a better and more idiomatic approach.

A counter-opinion to this choice is those who use a 3rd party HTTP request wrapper, like `fasthttp.Request` for those who use Fiber. Since it is a different shape than `http.Request`, the request will not be directly compatible with the argument to this function.

Patch:
diff --git a/interactions.go b/interactions.go
index abd0d55..f658a72 100644
--- a/interactions.go
+++ b/interactions.go
@@ -3,6 +3,8 @@ package slack
 import (
 	"bytes"
 	"encoding/json"
+	"errors"
+	"net/http"
 )
 
 // InteractionType type of interactions
@@ -74,6 +76,24 @@ type BlockActionStates struct {
 	Values map[string]map[string]BlockAction `json:"values"`
 }
 
+// InteractionCallbackParse parses the HTTP form value "payload" from r, unmarshals
+// it as JSON into an InteractionCallback, and returns the result.
+// It returns an error if the payload is missing or cannot be decoded.
+//
+// See https://github.com/slack-go/slack/issues/660 for context.
+func InteractionCallbackParse(r *http.Request) (InteractionCallback, error) {
+	payload := r.FormValue("payload")
+	if len(payload) == 0 {
+		return InteractionCallback{}, errors.New("payload is empty")
+	}
+
+	var ic InteractionCallback
+	if err := json.Unmarshal([]byte(payload), &ic); err != nil {
+		return InteractionCallback{}, err
+	}
+	return ic, nil
+}
+
 func (ic *InteractionCallback) MarshalJSON() ([]byte, error) {
 	type alias InteractionCallback
 	tmp := alias(*ic)


Test patch:
diff --git a/interactions_test.go b/interactions_test.go
index 3082809..5033206 100644
--- a/interactions_test.go
+++ b/interactions_test.go
@@ -2,6 +2,9 @@ package slack
 
 import (
 	"encoding/json"
+	"net/http"
+	"net/url"
+	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -548,3 +551,74 @@ func TestInteractionCallback_In_Thread_Container_Marshal_And_Unmarshal(t *testin
 	assert.NoError(t, err)
 	assert.Equal(t, expectedJSON, actualJSON)
 }
+
+func TestInteractionCallback_Parser(t *testing.T) {
+	payload := `{
+		"type": "block_actions",
+		"actions": [
+			{
+				"type": "multi_conversations_select",
+				"action_id": "multi_convos",
+				"block_id": "test123",
+				"selected_conversations": ["G12345"]
+			}
+		],
+		"container": {
+			"type": "view",
+			"view_id": "V12345"
+		},
+		"state": {
+			"values": {
+				"section_block_id": {
+					"multi_convos": {
+						"type": "multi_conversations_select",
+						"selected_conversations": ["G12345"]
+					}
+				},
+				"other_block_id": {
+					"other_action_id": {
+						"type": "plain_text_input",
+						"value": "test123"
+					}
+				}
+			}
+		}
+	}`
+
+	// create request body
+	body := url.Values{}
+	body.Set("payload", payload)
+
+	// create new request
+	req, err := http.NewRequest(http.MethodPost, "http://slack.example.org/interactions", strings.NewReader(body.Encode()))
+	assert.NoError(t, err)
+	assert.NotNil(t, req)
+
+	// without this header, the parser will not decode the payload
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+
+	// parse payload from request
+	ic, err := InteractionCallbackParse(req)
+	assert.NoError(t, err)
+	assert.NotNil(t, ic)
+
+	// test parsed InteractionCallback payload
+	assert.Equal(t, ic.Type, InteractionTypeBlockActions)
+
+	assert.Equal(t, len(ic.ActionCallback.BlockActions), 1)
+	assert.Equal(t, ic.ActionCallback.BlockActions[0].ActionID, "multi_convos")
+	assert.Equal(t, ic.ActionCallback.BlockActions[0].BlockID, "test123")
+	assert.Equal(t, ic.ActionCallback.BlockActions[0].Type, ActionType(MultiOptTypeConversations))
+	assert.Equal(t, len(ic.ActionCallback.BlockActions[0].SelectedConversations), 1)
+	assert.Equal(t, ic.ActionCallback.BlockActions[0].SelectedConversations[0], "G12345")
+
+	assert.Equal(t, ic.Container.Type, "view")
+	assert.Equal(t, ic.Container.ViewID, "V12345")
+
+	assert.Equal(t, len(ic.BlockActionState.Values), 2)
+	assert.Equal(t, ic.BlockActionState.Values["section_block_id"]["multi_convos"].Type, ActionType(MultiOptTypeConversations))
+	assert.Equal(t, len(ic.BlockActionState.Values["section_block_id"]["multi_convos"].SelectedConversations), 1)
+	assert.Equal(t, ic.BlockActionState.Values["section_block_id"]["multi_convos"].SelectedConversations[0], "G12345")
+	assert.Equal(t, ic.BlockActionState.Values["other_block_id"]["other_action_id"].Type, ActionType(METPlainTextInput))
+	assert.Equal(t, ic.BlockActionState.Values["other_block_id"]["other_action_id"].Value, "test123")
+}