Repository: slack-go/slack
Pull request #991 (2021-10-31 01:53:04)

Support Rich Text blocks
### Description
This PR supports `rich_text` blocks.
Once merging this, you'll be able to handle like following response correctly.

```json
  "blocks": [
    {
      "type": "rich_text",
      "block_id": "XXX",
      "elements": [
        {
          "type": "rich_text_section",
          "elements": [
            {
              "type": "channel",
              "channel_id": "C0123456789"
            },
            {
              "type": "text",
              "text": " hi"
            }
          ]
        }
      ]
    }
  ]

```

### References
- https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less
- https://github.com/slackapi/java-slack-sdk/blob/v1.13.0/slack-api-model/src/main/java/com/slack/api/model/block/element/RichTextSectionElement.java


Patch:
diff --git a/block.go b/block.go
index 3686db8..240f552 100644
--- a/block.go
+++ b/block.go
@@ -9,14 +9,15 @@ package slack
 type MessageBlockType string
 
 const (
-	MBTSection MessageBlockType = "section"
-	MBTDivider MessageBlockType = "divider"
-	MBTImage   MessageBlockType = "image"
-	MBTAction  MessageBlockType = "actions"
-	MBTContext MessageBlockType = "context"
-	MBTFile    MessageBlockType = "file"
-	MBTInput   MessageBlockType = "input"
-	MBTHeader  MessageBlockType = "header"
+	MBTSection  MessageBlockType = "section"
+	MBTDivider  MessageBlockType = "divider"
+	MBTImage    MessageBlockType = "image"
+	MBTAction   MessageBlockType = "actions"
+	MBTContext  MessageBlockType = "context"
+	MBTFile     MessageBlockType = "file"
+	MBTInput    MessageBlockType = "input"
+	MBTHeader   MessageBlockType = "header"
+	MBTRichText MessageBlockType = "rich_text"
 )
 
 // Block defines an interface all block types should implement
diff --git a/block_conv.go b/block_conv.go
index 6936700..c5378b6 100644
--- a/block_conv.go
+++ b/block_conv.go
@@ -65,6 +65,8 @@ func (b *Blocks) UnmarshalJSON(data []byte) error {
 			block = &ImageBlock{}
 		case "input":
 			block = &InputBlock{}
+		case "rich_text":
+			block = &RichTextBlock{}
 		case "section":
 			block = &SectionBlock{}
 		default:
diff --git a/block_element.go b/block_element.go
index 21abb01..bf06844 100644
--- a/block_element.go
+++ b/block_element.go
@@ -389,18 +389,13 @@ func NewTimePickerBlockElement(actionID string) *TimePickerBlockElement {
 //
 // More Information: https://api.slack.com/reference/block-kit/block-elements#input
 type PlainTextInputBlockElement struct {
-	Type                 MessageElementType    `json:"type"`
-	ActionID             string                `json:"action_id,omitempty"`
-	Placeholder          *TextBlockObject      `json:"placeholder,omitempty"`
-	InitialValue         string                `json:"initial_value,omitempty"`
-	Multiline            bool                  `json:"multiline,omitempty"`
-	MinLength            int                   `json:"min_length,omitempty"`
-	MaxLength            int                   `json:"max_length,omitempty"`
-	DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"`
-}
-
-type DispatchActionConfig struct {
-	TriggerActionsOn []string `json:"trigger_actions_on,omitempty"`
+	Type         MessageElementType `json:"type"`
+	ActionID     string             `json:"action_id,omitempty"`
+	Placeholder  *TextBlockObject   `json:"placeholder,omitempty"`
+	InitialValue string             `json:"initial_value,omitempty"`
+	Multiline    bool               `json:"multiline,omitempty"`
+	MinLength    int                `json:"min_length,omitempty"`
+	MaxLength    int                `json:"max_length,omitempty"`
 }
 
 // ElementType returns the type of the Element
diff --git a/block_rich_text.go b/block_rich_text.go
new file mode 100644
index 0000000..281db21
--- /dev/null
+++ b/block_rich_text.go
@@ -0,0 +1,383 @@
+package slack
+
+import (
+	"encoding/json"
+)
+
+// RichTextBlock defines a new block of type rich_text.
+// More Information: https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less
+type RichTextBlock struct {
+	Type     MessageBlockType  `json:"type"`
+	BlockID  string            `json:"block_id,omitempty"`
+	Elements []RichTextElement `json:"elements"`
+}
+
+func (b RichTextBlock) BlockType() MessageBlockType {
+	return b.Type
+}
+
+func (e *RichTextBlock) UnmarshalJSON(b []byte) error {
+	var raw struct {
+		Type        MessageBlockType  `json:"type"`
+		BlockID     string            `json:"block_id"`
+		RawElements []json.RawMessage `json:"elements"`
+	}
+	if string(b) == "{}" {
+		return nil
+	}
+	if err := json.Unmarshal(b, &raw); err != nil {
+		return err
+	}
+	elems := make([]RichTextElement, 0, len(raw.RawElements))
+	for _, r := range raw.RawElements {
+		var s struct {
+			Type RichTextElementType `json:"type"`
+		}
+		if err := json.Unmarshal(r, &s); err != nil {
+			return err
+		}
+		var elem RichTextElement
+		switch s.Type {
+		case RTESection:
+			elem = &RichTextSection{}
+		default:
+			elems = append(elems, &RichTextUnknown{
+				Type: s.Type,
+				Raw:  string(r),
+			})
+			continue
+		}
+		if err := json.Unmarshal(r, &elem); err != nil {
+			return err
+		}
+		elems = append(elems, elem)
+	}
+	*e = RichTextBlock{
+		Type:     raw.Type,
+		BlockID:  raw.BlockID,
+		Elements: elems,
+	}
+	return nil
+}
+
+// NewRichTextBlock returns a new instance of RichText Block.
+func NewRichTextBlock(blockID string, elements ...RichTextElement) *RichTextBlock {
+	return &RichTextBlock{
+		Type:     MBTRichText,
+		BlockID:  blockID,
+		Elements: elements,
+	}
+}
+
+type RichTextElementType string
+
+type RichTextElement interface {
+	RichTextElementType() RichTextElementType
+}
+
+const (
+	RTEList         RichTextElementType = "rich_text_list"
+	RTEPreformatted RichTextElementType = "rich_text_preformatted"
+	RTEQuote        RichTextElementType = "rich_text_quote"
+	RTESection      RichTextElementType = "rich_text_section"
+	RTEUnknown      RichTextElementType = "rich_text_unknown"
+)
+
+type RichTextUnknown struct {
+	Type RichTextElementType
+	Raw  string
+}
+
+func (u RichTextUnknown) RichTextElementType() RichTextElementType {
+	return u.Type
+}
+
+type RichTextSection struct {
+	Type     RichTextElementType      `json:"type"`
+	Elements []RichTextSectionElement `json:"elements"`
+}
+
+// ElementType returns the type of the Element
+func (s RichTextSection) RichTextElementType() RichTextElementType {
+	return s.Type
+}
+
+func (e *RichTextSection) UnmarshalJSON(b []byte) error {
+	var raw struct {
+		RawElements []json.RawMessage `json:"elements"`
+	}
+	if string(b) == "{}" {
+		return nil
+	}
+	if err := json.Unmarshal(b, &raw); err != nil {
+		return err
+	}
+	elems := make([]RichTextSectionElement, 0, len(raw.RawElements))
+	for _, r := range raw.RawElements {
+		var s struct {
+			Type RichTextSectionElementType `json:"type"`
+		}
+		if err := json.Unmarshal(r, &s); err != nil {
+			return err
+		}
+		var elem RichTextSectionElement
+		switch s.Type {
+		case RTSEText:
+			elem = &RichTextSectionTextElement{}
+		case RTSEChannel:
+			elem = &RichTextSectionChannelElement{}
+		case RTSEUser:
+			elem = &RichTextSectionUserElement{}
+		case RTSEEmoji:
+			elem = &RichTextSectionEmojiElement{}
+		case RTSELink:
+			elem = &RichTextSectionLinkElement{}
+		case RTSETeam:
+			elem = &RichTextSectionTeamElement{}
+		case RTSEUserGroup:
+			elem = &RichTextSectionUserGroupElement{}
+		case RTSEDate:
+			elem = &RichTextSectionDateElement{}
+		case RTSEBroadcast:
+			elem = &RichTextSectionBroadcastElement{}
+		case RTSEColor:
+			elem = &RichTextSectionColorElement{}
+		default:
+			elems = append(elems, &RichTextSectionUnknownElement{
+				Type: s.Type,
+				Raw:  string(r),
+			})
+			continue
+		}
+		if err := json.Unmarshal(r, elem); err != nil {
+			return err
+		}
+		elems = append(elems, elem)
+	}
+	*e = RichTextSection{
+		Type:     RTESection,
+		Elements: elems,
+	}
+	return nil
+}
+
+// NewRichTextSectionBlockElement .
+func NewRichTextSection(elements ...RichTextSectionElement) *RichTextSection {
+	return &RichTextSection{
+		Type:     RTESection,
+		Elements: elements,
+	}
+}
+
+type RichTextSectionElementType string
+
+const (
+	RTSEBroadcast RichTextSectionElementType = "broadcast"
+	RTSEChannel   RichTextSectionElementType = "channel"
+	RTSEColor     RichTextSectionElementType = "color"
+	RTSEDate      RichTextSectionElementType = "date"
+	RTSEEmoji     RichTextSectionElementType = "emoji"
+	RTSELink      RichTextSectionElementType = "link"
+	RTSETeam      RichTextSectionElementType = "team"
+	RTSEText      RichTextSectionElementType = "text"
+	RTSEUser      RichTextSectionElementType = "user"
+	RTSEUserGroup RichTextSectionElementType = "usergroup"
+
+	RTSEUnknown RichTextSectionElementType = "unknown"
+)
+
+type RichTextSectionElement interface {
+	RichTextSectionElementType() RichTextSectionElementType
+}
+
+type RichTextSectionTextStyle struct {
+	Bold   bool `json:"bold,omitempty"`
+	Italic bool `json:"italic,omitempty"`
+	Strike bool `json:"strike,omitempty"`
+	Code   bool `json:"code,omitempty"`
+}
+
+type RichTextSectionTextElement struct {
+	Type  RichTextSectionElementType `json:"type"`
+	Text  string                     `json:"text"`
+	Style *RichTextSectionTextStyle  `json:"style,omitempty"`
+}
+
+func (r RichTextSectionTextElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionTextElement(text string, style *RichTextSectionTextStyle) *RichTextSectionTextElement {
+	return &RichTextSectionTextElement{
+		Type:  RTSEText,
+		Text:  text,
+		Style: style,
+	}
+}
+
+type RichTextSectionChannelElement struct {
+	Type      RichTextSectionElementType `json:"type"`
+	ChannelID string                     `json:"channel_id"`
+	Style     *RichTextSectionTextStyle  `json:"style,omitempty"`
+}
+
+func (r RichTextSectionChannelElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionChannelElement(channelID string, style *RichTextSectionTextStyle) *RichTextSectionChannelElement {
+	return &RichTextSectionChannelElement{
+		Type:      RTSEText,
+		ChannelID: channelID,
+		Style:     style,
+	}
+}
+
+type RichTextSectionUserElement struct {
+	Type   RichTextSectionElementType `json:"type"`
+	UserID string                     `json:"user_id"`
+	Style  *RichTextSectionTextStyle  `json:"style,omitempty"`
+}
+
+func (r RichTextSectionUserElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionUserElement(userID string, style *RichTextSectionTextStyle) *RichTextSectionUserElement {
+	return &RichTextSectionUserElement{
+		Type:   RTSEUser,
+		UserID: userID,
+		Style:  style,
+	}
+}
+
+type RichTextSectionEmojiElement struct {
+	Type     RichTextSectionElementType `json:"type"`
+	Name     string                     `json:"name"`
+	SkinTone int                        `json:"skin_tone"`
+	Style    *RichTextSectionTextStyle  `json:"style,omitempty"`
+}
+
+func (r RichTextSectionEmojiElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionEmojiElement(name string, skinTone int, style *RichTextSectionTextStyle) *RichTextSectionEmojiElement {
+	return &RichTextSectionEmojiElement{
+		Type:     RTSEEmoji,
+		Name:     name,
+		SkinTone: skinTone,
+		Style:    style,
+	}
+}
+
+type RichTextSectionLinkElement struct {
+	Type  RichTextSectionElementType `json:"type"`
+	URL   string                     `json:"url"`
+	Text  string                     `json:"text"`
+	Style *RichTextSectionTextStyle  `json:"style,omitempty"`
+}
+
+func (r RichTextSectionLinkElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionLinkElement(url, text string, style *RichTextSectionTextStyle) *RichTextSectionLinkElement {
+	return &RichTextSectionLinkElement{
+		Type:  RTSELink,
+		URL:   url,
+		Text:  text,
+		Style: style,
+	}
+}
+
+type RichTextSectionTeamElement struct {
+	Type   RichTextSectionElementType `json:"type"`
+	TeamID string                     `json:"team_id"`
+	Style  *RichTextSectionTextStyle  `json:"style.omitempty"`
+}
+
+func (r RichTextSectionTeamElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionTeamElement(teamID string, style *RichTextSectionTextStyle) *RichTextSectionTeamElement {
+	return &RichTextSectionTeamElement{
+		Type:   RTSETeam,
+		TeamID: teamID,
+		Style:  style,
+	}
+}
+
+type RichTextSectionUserGroupElement struct {
+	Type        RichTextSectionElementType `json:"type"`
+	UsergroupID string                     `json:"usergroup_id"`
+}
+
+func (r RichTextSectionUserGroupElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionUserGroupElement(usergroupID string) *RichTextSectionUserGroupElement {
+	return &RichTextSectionUserGroupElement{
+		Type:        RTSEUserGroup,
+		UsergroupID: usergroupID,
+	}
+}
+
+type RichTextSectionDateElement struct {
+	Type      RichTextSectionElementType `json:"type"`
+	Timestamp string                     `json:"timestamp"`
+}
+
+func (r RichTextSectionDateElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionDateElement(timestamp string) *RichTextSectionDateElement {
+	return &RichTextSectionDateElement{
+		Type:      RTSEDate,
+		Timestamp: timestamp,
+	}
+}
+
+type RichTextSectionBroadcastElement struct {
+	Type  RichTextSectionElementType `json:"type"`
+	Range string                     `json:"range"`
+}
+
+func (r RichTextSectionBroadcastElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionBroadcastElement(rangeStr string) *RichTextSectionBroadcastElement {
+	return &RichTextSectionBroadcastElement{
+		Type:  RTSEBroadcast,
+		Range: rangeStr,
+	}
+}
+
+type RichTextSectionColorElement struct {
+	Type  RichTextSectionElementType `json:"type"`
+	Value string                     `json:"value"`
+}
+
+func (r RichTextSectionColorElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
+
+func NewRichTextSectionColorElement(value string) *RichTextSectionColorElement {
+	return &RichTextSectionColorElement{
+		Type:  RTSEColor,
+		Value: value,
+	}
+}
+
+type RichTextSectionUnknownElement struct {
+	Type RichTextSectionElementType `json:"type"`
+	Raw  string
+}
+
+func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSectionElementType {
+	return r.Type
+}
diff --git a/examples/socketmode/socketmode.go b/examples/socketmode/socketmode.go
index 6f1e65b..69e34c2 100644
--- a/examples/socketmode/socketmode.go
+++ b/examples/socketmode/socketmode.go
@@ -15,8 +15,7 @@ import (
 func main() {
 	appToken := os.Getenv("SLACK_APP_TOKEN")
 	if appToken == "" {
-		fmt.Fprintf(os.Stderr, "SLACK_APP_TOKEN must be set.\n")
-		os.Exit(1)
+
 	}
 
 	if !strings.HasPrefix(appToken, "xapp-") {
diff --git a/misc.go b/misc.go
index 5272e7c..8c50305 100644
--- a/misc.go
+++ b/misc.go
@@ -41,17 +41,9 @@ func (t SlackResponse) Err() error {
 		return nil
 	}
 
-	return SlackErrorResponse{Err: t.Error, ResponseMetadata: t.ResponseMetadata}
+	return errors.New(t.Error)
 }
 
-// SlackErrorResponse brings along the metadata of errors returned by the Slack API.
-type SlackErrorResponse struct {
-	Err              string
-	ResponseMetadata ResponseMetadata
-}
-
-func (r SlackErrorResponse) Error() string { return r.Err }
-
 // RateLimitedError represents the rate limit respond from slack
 type RateLimitedError struct {
 	RetryAfter time.Duration


Test patch:
diff --git a/block_rich_text_test.go b/block_rich_text_test.go
new file mode 100644
index 0000000..4c889eb
--- /dev/null
+++ b/block_rich_text_test.go
@@ -0,0 +1,118 @@
+package slack
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/go-test/deep"
+)
+
+const (
+	dummyPayload = `{
+  "type":"rich_text",
+  "block_id":"FaYCD",
+  "elements": [
+    {
+      "type":"rich_text_section",
+      "elements": [
+        {
+          "type":"channel",
+          "channel_id":"C012345678"
+        },
+        {
+          "type":"text",
+          "text":"dummy_text"
+        }
+      ]
+    }
+  ]
+}`
+)
+
+func TestRichTextBlock_UnmarshalJSON(t *testing.T) {
+	cases := []struct {
+		raw      []byte
+		expected RichTextBlock
+		err      error
+	}{
+		{
+			[]byte(`{"elements":[{"type":"rich_text_unknown"},{"type":"rich_text_section"}]}`),
+			RichTextBlock{
+				Elements: []RichTextElement{
+					&RichTextUnknown{Type: RTEUnknown, Raw: `{"type":"rich_text_unknown"}`},
+					&RichTextSection{Type: RTESection, Elements: []RichTextSectionElement{}},
+				},
+			},
+			nil,
+		},
+		{
+			[]byte(`{"type": "rich_text","block_id":"blk","elements":[]}`),
+			RichTextBlock{
+				Type:     MBTRichText,
+				BlockID:  "blk",
+				Elements: []RichTextElement{},
+			},
+			nil,
+		},
+	}
+	for _, tc := range cases {
+		var actual RichTextBlock
+		err := json.Unmarshal(tc.raw, &actual)
+		if err != nil {
+			if tc.err == nil {
+				t.Errorf("unexpected error: %s", err)
+			}
+			t.Errorf("expected error is %s, but got %s", tc.err, err)
+		}
+		if tc.err != nil {
+			t.Errorf("expected to raise an error %s", tc.err)
+		}
+		if diff := deep.Equal(actual, tc.expected); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	}
+}
+
+func TestRichTextSection_UnmarshalJSON(t *testing.T) {
+	cases := []struct {
+		raw      []byte
+		expected RichTextSection
+		err      error
+	}{
+		{
+			[]byte(`{"elements":[{"type":"unknown","value":10},{"type":"text","text":"hi"}]}`),
+			RichTextSection{
+				Type: RTESection,
+				Elements: []RichTextSectionElement{
+					&RichTextSectionUnknownElement{Type: RTSEUnknown, Raw: `{"type":"unknown","value":10}`},
+					&RichTextSectionTextElement{Type: RTSEText, Text: "hi"},
+				},
+			},
+			nil,
+		},
+		{
+			[]byte(`{"type": "rich_text_section","elements":[]}`),
+			RichTextSection{
+				Type:     RTESection,
+				Elements: []RichTextSectionElement{},
+			},
+			nil,
+		},
+	}
+	for _, tc := range cases {
+		var actual RichTextSection
+		err := json.Unmarshal(tc.raw, &actual)
+		if err != nil {
+			if tc.err == nil {
+				t.Errorf("unexpected error: %s", err)
+			}
+			t.Errorf("expected error is %s, but got %s", tc.err, err)
+		}
+		if tc.err != nil {
+			t.Errorf("expected to raise an error %s", tc.err)
+		}
+		if diff := deep.Equal(actual, tc.expected); diff != nil {
+			t.Errorf("actual value does not match expected one\n%s", diff)
+		}
+	}
+}