Repository: slack-go/slack
Pull request #820 (2020-10-09 20:19:32)

Handle state in interaction callback
The type of state field in a Slack response to interactons now depends
on the type of interaction. Specifically, the correspondence is as
follows.

- type: dialog_submisson -> state: string
- type: block_actions -> state: dictionary array

This commit is trying to handle these properly while maintaining backwards
compatibility, so futureversions may change to a more approproate design.

See Also: https://api.slack.com/changelog/2020-09-full-state-on-view-submisson-and-block-actions

resolved: #816

@ollieparsley What do you think? Please review this PR 🙏 


Patch:
diff --git a/dialog.go b/dialog.go
index 376cd9e..f94113f 100644
--- a/dialog.go
+++ b/dialog.go
@@ -54,7 +54,9 @@ type DialogCallback InteractionCallback
 
 // DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog
 type DialogSubmissionCallback struct {
-	State      string            `json:"state,omitempty"`
+	// NOTE: State is only used with the dialog_submission type.
+	// You should use InteractionCallback.BlockActionsState for block_actions type.
+	State      string            `json:"-"`
 	Submission map[string]string `json:"submission"`
 }
 
diff --git a/interactions.go b/interactions.go
index c7f5921..34ab9f7 100644
--- a/interactions.go
+++ b/interactions.go
@@ -56,6 +56,63 @@ type InteractionCallback struct {
 	DialogSubmissionCallback
 	ViewSubmissionCallback
 	ViewClosedCallback
+
+	// FIXME(kanata2): just workaround for backward-compatibility.
+	// See also https://github.com/slack-go/slack/issues/816
+	RawState json.RawMessage `json:"state,omitempty"`
+
+	// BlockActionState stands for the `state` field in block_actions type.
+	// NOTE: InteractionCallback.State has a role for the state of dialog_submission type,
+	// so we cannot use this field for backward-compatibility for now.
+	BlockActionState *BlockActionStates `json:"-"`
+}
+
+type BlockActionStates struct {
+	Values map[string]map[string]BlockAction `json:"values"`
+}
+
+func (ic *InteractionCallback) MarshalJSON() ([]byte, error) {
+	type alias InteractionCallback
+	tmp := alias(*ic)
+	if tmp.Type == InteractionTypeBlockActions {
+		if tmp.BlockActionState == nil {
+			tmp.RawState = []byte(`{}`)
+		} else {
+			state, err := json.Marshal(tmp.BlockActionState.Values)
+			if err != nil {
+				return nil, err
+			}
+			tmp.RawState = []byte(`{"values":` + string(state) + `}`)
+		}
+	} else if ic.Type == InteractionTypeDialogSubmission {
+		tmp.RawState = []byte(tmp.State)
+	}
+	// Use pointer for go1.7
+	return json.Marshal(&tmp)
+}
+
+func (ic *InteractionCallback) UnmarshalJSON(b []byte) error {
+	type alias InteractionCallback
+	tmp := struct {
+		Type InteractionType `json:"type"`
+		*alias
+	}{
+		alias: (*alias)(ic),
+	}
+	if err := json.Unmarshal(b, &tmp); err != nil {
+		return err
+	}
+	*ic = InteractionCallback(*tmp.alias)
+	ic.Type = tmp.Type
+	if ic.Type == InteractionTypeBlockActions {
+		err := json.Unmarshal(ic.RawState, &ic.BlockActionState)
+		if err != nil {
+			return err
+		}
+	} else if ic.Type == InteractionTypeDialogSubmission {
+		ic.State = string(ic.RawState)
+	}
+	return nil
 }
 
 type Container struct {


Test patch:
diff --git a/interactions_test.go b/interactions_test.go
index 6298a5a..c4f98b8 100644
--- a/interactions_test.go
+++ b/interactions_test.go
@@ -304,7 +304,8 @@ func TestInteractionCallbackJSONMarshalAndUnmarshal(t *testing.T) {
 				BlockSet: []Block{NewDividerBlock()},
 			},
 		},
-		DialogSubmissionCallback: DialogSubmissionCallback{State: "dsstate"},
+		DialogSubmissionCallback: DialogSubmissionCallback{State: ""},
+		RawState:                 json.RawMessage(`{}`),
 	}
 
 	cbJSONBytes, err := json.Marshal(cb)
@@ -347,3 +348,46 @@ func TestInteractionCallbackJSONMarshalAndUnmarshal(t *testing.T) {
 	assert.Equal(t, cb.DialogSubmissionCallback.State,
 		jsonCB.DialogSubmissionCallback.State)
 }
+
+func TestInteractionCallback_InteractionTypeBlockActions_Unmarshal(t *testing.T) {
+	raw := []byte(`{
+		"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"
+					}
+				}
+			}
+		}
+	}`)
+	var cb InteractionCallback
+	assert.NoError(t, json.Unmarshal(raw, &cb))
+	assert.Equal(t, cb.State, "")
+	assert.Equal(t,
+		cb.BlockActionState.Values["section_block_id"]["multi_convos"].actionType(),
+		actionType(MultiOptTypeConversations))
+	assert.Equal(t,
+		cb.BlockActionState.Values["section_block_id"]["multi_convos"].SelectedConversations,
+		[]string{"G12345"})
+}