package slack import ( "bytes" "encoding/json" ) // InteractionType type of interactions type InteractionType string // ActionType type represents the type of action (attachment, block, etc.) type actionType string // action is an interface that should be implemented by all callback action types type action interface { actionType() actionType } // Types of interactions that can be received. const ( InteractionTypeDialogCancellation = InteractionType("dialog_cancellation") InteractionTypeDialogSubmission = InteractionType("dialog_submission") InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion") InteractionTypeInteractionMessage = InteractionType("interactive_message") InteractionTypeMessageAction = InteractionType("message_action") InteractionTypeBlockActions = InteractionType("block_actions") InteractionTypeBlockSuggestion = InteractionType("block_suggestion") InteractionTypeViewSubmission = InteractionType("view_submission") InteractionTypeViewClosed = InteractionType("view_closed") InteractionTypeShortcut = InteractionType("shortcut") ) // InteractionCallback is sent from slack when a user interactions with a button or dialog. type InteractionCallback struct { Type InteractionType `json:"type"` Token string `json:"token"` CallbackID string `json:"callback_id"` ResponseURL string `json:"response_url"` TriggerID string `json:"trigger_id"` ActionTs string `json:"action_ts"` Team Team `json:"team"` Channel Channel `json:"channel"` User User `json:"user"` OriginalMessage Message `json:"original_message"` Message Message `json:"message"` Name string `json:"name"` Value string `json:"value"` MessageTs string `json:"message_ts"` AttachmentID string `json:"attachment_id"` ActionCallback ActionCallbacks `json:"actions"` View View `json:"view"` ActionID string `json:"action_id"` APIAppID string `json:"api_app_id"` BlockID string `json:"block_id"` Container Container `json:"container"` 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 { if len(ic.RawState) > 0 { 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 { Type string `json:"type"` ViewID string `json:"view_id"` MessageTs string `json:"message_ts"` AttachmentID json.Number `json:"attachment_id"` ChannelID string `json:"channel_id"` IsEphemeral bool `json:"is_ephemeral"` IsAppUnfurl bool `json:"is_app_unfurl"` } // ActionCallback is a convenience struct defined to allow dynamic unmarshalling of // the "actions" value in Slack's JSON response, which varies depending on block type type ActionCallbacks struct { AttachmentActions []*AttachmentAction BlockActions []*BlockAction } // MarshalJSON implements the Marshaller interface in order to combine both // action callback types back into a single array, like how the api responds. // This makes Marshaling and Unmarshaling an InteractionCallback symmetrical func (a ActionCallbacks) MarshalJSON() ([]byte, error) { count := 0 length := len(a.AttachmentActions) + len(a.BlockActions) buffer := bytes.NewBufferString("[") f := func(obj interface{}) error { js, err := json.Marshal(obj) if err != nil { return err } _, err = buffer.Write(js) if err != nil { return err } count++ if count < length { _, err = buffer.WriteString(",") return err } return nil } for _, act := range a.AttachmentActions { err := f(act) if err != nil { return nil, err } } for _, blk := range a.BlockActions { err := f(blk) if err != nil { return nil, err } } buffer.WriteString("]") return buffer.Bytes(), nil } // UnmarshalJSON implements the Marshaller interface in order to delegate // marshalling and allow for proper type assertion when decoding the response func (a *ActionCallbacks) UnmarshalJSON(data []byte) error { var raw []json.RawMessage err := json.Unmarshal(data, &raw) if err != nil { return err } for _, r := range raw { var obj map[string]interface{} err := json.Unmarshal(r, &obj) if err != nil { return err } if _, ok := obj["block_id"].(string); ok { action, err := unmarshalAction(r, &BlockAction{}) if err != nil { return err } a.BlockActions = append(a.BlockActions, action.(*BlockAction)) continue } action, err := unmarshalAction(r, &AttachmentAction{}) if err != nil { return err } a.AttachmentActions = append(a.AttachmentActions, action.(*AttachmentAction)) } return nil } func unmarshalAction(r json.RawMessage, callbackAction action) (action, error) { err := json.Unmarshal(r, callbackAction) if err != nil { return nil, err } return callbackAction, nil }