mirror of
https://github.com/paymog/slack-cli.git
synced 2026-09-18 23:43:49 +08:00
Merge pull request #190 from derodero24/feat/extract-block-kit-text
feat: extract text content from message blocks in conversations_history
This commit is contained in:
@@ -1533,7 +1533,11 @@ func (ch *ConversationsHandler) convertMessagesFromHistory(slackMessages []slack
|
||||
continue
|
||||
}
|
||||
|
||||
msgText := msg.Text + text.AttachmentsTo2CSV(msg.Text, msg.Attachments)
|
||||
msgText := msg.Text
|
||||
if msgText == "" {
|
||||
msgText = text.BlocksToText(msg.Blocks)
|
||||
}
|
||||
msgText += text.AttachmentsTo2CSV(msgText, msg.Attachments)
|
||||
|
||||
var reactionParts []string
|
||||
for _, r := range msg.Reactions {
|
||||
@@ -1609,7 +1613,11 @@ func (ch *ConversationsHandler) convertMessagesFromSearch(slackMessages []slack.
|
||||
continue
|
||||
}
|
||||
|
||||
msgText := msg.Text + text.AttachmentsTo2CSV(msg.Text, msg.Attachments)
|
||||
msgText := msg.Text
|
||||
if msgText == "" {
|
||||
msgText = text.BlocksToText(msg.Blocks)
|
||||
}
|
||||
msgText += text.AttachmentsTo2CSV(msgText, msg.Attachments)
|
||||
|
||||
hasMedia := hasImageBlocks(msg.Blocks)
|
||||
|
||||
|
||||
@@ -39,6 +39,10 @@ func AttachmentToText(att slack.Attachment) string {
|
||||
parts = append(parts, fmt.Sprintf("Footer: %s @ %s", att.Footer, ts))
|
||||
}
|
||||
|
||||
if blocksText := BlocksToText(att.Blocks); blocksText != "" {
|
||||
parts = append(parts, fmt.Sprintf("Blocks: %s", blocksText))
|
||||
}
|
||||
|
||||
result := strings.Join(parts, "; ")
|
||||
|
||||
result = strings.ReplaceAll(result, "\n", " ")
|
||||
@@ -51,6 +55,102 @@ func AttachmentToText(att slack.Attachment) string {
|
||||
return result
|
||||
}
|
||||
|
||||
// BlocksToText extracts text content from Slack Block Kit structures.
|
||||
func BlocksToText(blocks slack.Blocks) string {
|
||||
if len(blocks.BlockSet) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
var parts []string
|
||||
|
||||
for _, block := range blocks.BlockSet {
|
||||
switch b := block.(type) {
|
||||
case *slack.HeaderBlock:
|
||||
if b.Text != nil && b.Text.Text != "" {
|
||||
parts = append(parts, b.Text.Text)
|
||||
}
|
||||
case *slack.SectionBlock:
|
||||
if b.Text != nil && b.Text.Text != "" {
|
||||
parts = append(parts, b.Text.Text)
|
||||
}
|
||||
for _, field := range b.Fields {
|
||||
if field != nil && field.Text != "" {
|
||||
parts = append(parts, field.Text)
|
||||
}
|
||||
}
|
||||
case *slack.RichTextBlock:
|
||||
if t := richTextBlockToText(b); t != "" {
|
||||
parts = append(parts, t)
|
||||
}
|
||||
case *slack.ContextBlock:
|
||||
for _, elem := range b.ContextElements.Elements {
|
||||
if txt, ok := elem.(*slack.TextBlockObject); ok && txt != nil && txt.Text != "" {
|
||||
parts = append(parts, txt.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func richTextBlockToText(rtb *slack.RichTextBlock) string {
|
||||
var parts []string
|
||||
|
||||
for _, elem := range rtb.Elements {
|
||||
if t := richTextElementToText(elem); t != "" {
|
||||
parts = append(parts, t)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, " ")
|
||||
}
|
||||
|
||||
func richTextElementToText(elem slack.RichTextElement) string {
|
||||
switch e := elem.(type) {
|
||||
case *slack.RichTextSection:
|
||||
return richTextSectionToText(e)
|
||||
case *slack.RichTextList:
|
||||
var parts []string
|
||||
for _, listElem := range e.Elements {
|
||||
if t := richTextElementToText(listElem); t != "" {
|
||||
parts = append(parts, t)
|
||||
}
|
||||
}
|
||||
return strings.Join(parts, " ")
|
||||
case *slack.RichTextQuote:
|
||||
return richTextSectionToText((*slack.RichTextSection)(e))
|
||||
case *slack.RichTextPreformatted:
|
||||
return richTextSectionToText(&e.RichTextSection)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func richTextSectionToText(section *slack.RichTextSection) string {
|
||||
var parts []string
|
||||
|
||||
for _, elem := range section.Elements {
|
||||
switch e := elem.(type) {
|
||||
case *slack.RichTextSectionTextElement:
|
||||
if e.Text != "" {
|
||||
parts = append(parts, e.Text)
|
||||
}
|
||||
case *slack.RichTextSectionLinkElement:
|
||||
if e.Text != "" {
|
||||
parts = append(parts, e.Text)
|
||||
} else if e.URL != "" {
|
||||
parts = append(parts, e.URL)
|
||||
}
|
||||
case *slack.RichTextSectionBroadcastElement:
|
||||
if e.Range != "" {
|
||||
parts = append(parts, "@"+e.Range)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(parts, "")
|
||||
}
|
||||
|
||||
func AttachmentsTo2CSV(msgText string, attachments []slack.Attachment) string {
|
||||
if len(attachments) == 0 {
|
||||
return ""
|
||||
|
||||
@@ -2,8 +2,312 @@ package text
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/slack-go/slack"
|
||||
)
|
||||
|
||||
func TestBlocksToText(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
blocks slack.Blocks
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "empty blocks",
|
||||
blocks: slack.Blocks{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "header block",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.HeaderBlock{
|
||||
Type: slack.MBTHeader,
|
||||
Text: &slack.TextBlockObject{
|
||||
Type: "plain_text",
|
||||
Text: "Important Header",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "Important Header",
|
||||
},
|
||||
{
|
||||
name: "section block with text",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.SectionBlock{
|
||||
Type: slack.MBTSection,
|
||||
Text: &slack.TextBlockObject{
|
||||
Type: "mrkdwn",
|
||||
Text: "Hello from section",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "Hello from section",
|
||||
},
|
||||
{
|
||||
name: "section block with fields",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.SectionBlock{
|
||||
Type: slack.MBTSection,
|
||||
Text: &slack.TextBlockObject{
|
||||
Type: "mrkdwn",
|
||||
Text: "Main text",
|
||||
},
|
||||
Fields: []*slack.TextBlockObject{
|
||||
{Type: "mrkdwn", Text: "Field 1"},
|
||||
{Type: "mrkdwn", Text: "Field 2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "Main text Field 1 Field 2",
|
||||
},
|
||||
{
|
||||
name: "context block",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.ContextBlock{
|
||||
Type: slack.MBTContext,
|
||||
ContextElements: slack.ContextElements{
|
||||
Elements: []slack.MixedElement{
|
||||
&slack.TextBlockObject{
|
||||
Type: "mrkdwn",
|
||||
Text: "context info",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "context info",
|
||||
},
|
||||
{
|
||||
name: "rich text block with text elements",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.RichTextBlock{
|
||||
Type: slack.MBTRichText,
|
||||
Elements: []slack.RichTextElement{
|
||||
&slack.RichTextSection{
|
||||
Type: slack.RTESection,
|
||||
Elements: []slack.RichTextSectionElement{
|
||||
&slack.RichTextSectionTextElement{
|
||||
Type: slack.RTSEText,
|
||||
Text: "Hello ",
|
||||
},
|
||||
&slack.RichTextSectionTextElement{
|
||||
Type: slack.RTSEText,
|
||||
Text: "World",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "Hello World",
|
||||
},
|
||||
{
|
||||
name: "rich text block with link element",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.RichTextBlock{
|
||||
Type: slack.MBTRichText,
|
||||
Elements: []slack.RichTextElement{
|
||||
&slack.RichTextSection{
|
||||
Type: slack.RTESection,
|
||||
Elements: []slack.RichTextSectionElement{
|
||||
&slack.RichTextSectionTextElement{
|
||||
Type: slack.RTSEText,
|
||||
Text: "Click ",
|
||||
},
|
||||
&slack.RichTextSectionLinkElement{
|
||||
Type: slack.RTSELink,
|
||||
URL: "https://example.com",
|
||||
Text: "here",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "Click here",
|
||||
},
|
||||
{
|
||||
name: "rich text link without display text falls back to URL",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.RichTextBlock{
|
||||
Type: slack.MBTRichText,
|
||||
Elements: []slack.RichTextElement{
|
||||
&slack.RichTextSection{
|
||||
Type: slack.RTESection,
|
||||
Elements: []slack.RichTextSectionElement{
|
||||
&slack.RichTextSectionLinkElement{
|
||||
Type: slack.RTSELink,
|
||||
URL: "https://example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "https://example.com",
|
||||
},
|
||||
{
|
||||
name: "multiple blocks combined",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.HeaderBlock{
|
||||
Type: slack.MBTHeader,
|
||||
Text: &slack.TextBlockObject{
|
||||
Type: "plain_text",
|
||||
Text: "Alert",
|
||||
},
|
||||
},
|
||||
&slack.SectionBlock{
|
||||
Type: slack.MBTSection,
|
||||
Text: &slack.TextBlockObject{
|
||||
Type: "mrkdwn",
|
||||
Text: "Server is down",
|
||||
},
|
||||
},
|
||||
&slack.ContextBlock{
|
||||
Type: slack.MBTContext,
|
||||
ContextElements: slack.ContextElements{
|
||||
Elements: []slack.MixedElement{
|
||||
&slack.TextBlockObject{
|
||||
Type: "plain_text",
|
||||
Text: "sent by monitoring",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "Alert Server is down sent by monitoring",
|
||||
},
|
||||
{
|
||||
name: "divider and image blocks are skipped",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.SectionBlock{
|
||||
Type: slack.MBTSection,
|
||||
Text: &slack.TextBlockObject{
|
||||
Type: "mrkdwn",
|
||||
Text: "visible text",
|
||||
},
|
||||
},
|
||||
&slack.DividerBlock{
|
||||
Type: slack.MBTDivider,
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "visible text",
|
||||
},
|
||||
{
|
||||
name: "rich text with broadcast element",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.RichTextBlock{
|
||||
Type: slack.MBTRichText,
|
||||
Elements: []slack.RichTextElement{
|
||||
&slack.RichTextSection{
|
||||
Type: slack.RTESection,
|
||||
Elements: []slack.RichTextSectionElement{
|
||||
&slack.RichTextSectionBroadcastElement{
|
||||
Type: slack.RTSEBroadcast,
|
||||
Range: "channel",
|
||||
},
|
||||
&slack.RichTextSectionTextElement{
|
||||
Type: slack.RTSEText,
|
||||
Text: " please review",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "@channel please review",
|
||||
},
|
||||
{
|
||||
name: "section block with nil text",
|
||||
blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.SectionBlock{
|
||||
Type: slack.MBTSection,
|
||||
Fields: []*slack.TextBlockObject{
|
||||
{Type: "mrkdwn", Text: "Only fields"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: "Only fields",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := BlocksToText(tt.blocks)
|
||||
if got != tt.want {
|
||||
t.Errorf("BlocksToText() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachmentToTextWithBlocks(t *testing.T) {
|
||||
att := slack.Attachment{
|
||||
Blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.SectionBlock{
|
||||
Type: slack.MBTSection,
|
||||
Text: &slack.TextBlockObject{
|
||||
Type: "mrkdwn",
|
||||
Text: "block content",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := AttachmentToText(att)
|
||||
if result != "Blocks: block content" {
|
||||
t.Errorf("AttachmentToText() = %q, want %q", result, "Blocks: block content")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttachmentToTextWithBlocksAndText(t *testing.T) {
|
||||
att := slack.Attachment{
|
||||
Title: "Email Subject",
|
||||
Text: "email body",
|
||||
Blocks: slack.Blocks{
|
||||
BlockSet: []slack.Block{
|
||||
&slack.SectionBlock{
|
||||
Type: slack.MBTSection,
|
||||
Text: &slack.TextBlockObject{
|
||||
Type: "mrkdwn",
|
||||
Text: "extra block info",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
result := AttachmentToText(att)
|
||||
expected := "Title: Email Subject; Text: email body; Blocks: extra block info"
|
||||
if result != expected {
|
||||
t.Errorf("AttachmentToText() = %q, want %q", result, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUnfurlingEnabled(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user