Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chat.update: add support for sending empty blocks #1214

Open
batazor opened this issue Jul 12, 2023 · 1 comment
Open

chat.update: add support for sending empty blocks #1214

batazor opened this issue Jul 12, 2023 · 1 comment

Comments

@batazor
Copy link

batazor commented Jul 12, 2023

What happened

I'm developing a bot for an employee survey. When the employee selects the answer option - yes/no - I want to replace the block with a text field answer, however, slack-go does a check on the length of the blocks field and does not add ?blocks=[] to the request to the Slack API. Without that field, the API says the update was successful, but really doesn't change the message

Expected behavior

The ?blocks=[] field is added to the query

Steps to reproduce

  1. Create message with blocks
[
    {
      "type":"actions",
      "elements":[
        {"type":"button","text":{"type":"plain_text","text":"Approve"},"value":"click_me_123"},
        {"type":"button","text":{"type":"plain_text","text":"Deny"},"value":"click_me_123"}
      ]
    }
]
  1. Update message without blocks

reproducible code

manifest.yaml

Versions

  • Go: 1.19/1.20
  • slack-go/slack: 0.12.2
@TopherGopher
Copy link

Can confirm this issue is still present. If you add a block, then the update works great, as intended, but you can't update using text.

I've done my best to recreate some reproducible code - but you may need to tweak it a little:

package chatbot

import (
	"fmt"
	"github.com/slack-go/slack"
	"github.com/stretchr/testify/assert"
	"testing"
)

var slackClient *slack.Client
// ErrChannelNotFound can be used with errors.Is to determine if the channel
// doesn't exist
var ErrChannelNotFound = errors.New("channel_not_found")

func setup(t *testing.T) {
	t.Helper()
	slackClient = slack.New(slackBotToken,
		slack.OptionAppLevelToken(slackAppToken))
}

// ChatMessage is an abstraction, designed to make chat input/output more accessible from other components
// You should instantiate this by using NewChatMessage()
type ChatMessage struct {
	ShortMessage    string             `bson:"shortMessage" json:"shortMessage"`
	ChannelName     string             `bson:"channelName" json:"channelName"`
	SuccessResponse string             `bson:"successResponse" json:"successResponse"`
	FailureResponse string             `bson:"failureResponse" json:"failureResponse"`
	// AtMentionUser is the user to mention in the message, e.g. topher.sterling
	//     - there should not be an @prefix
	AtMentionUser string                 `bson:"atMentionUser" json:"atMentionUser"`
	Metadata      map[string]interface{} `bson:"metadata" json:"metadata"`
	Blocks        []CFChatbotBlock       `bson:"-" json:"-"`
}

func NewChatMessage(shortMessage, channelName string) *ChatMessage {
	c := &ChatMessage{
		ChannelName:  channelName,
		ShortMessage: shortMessage,
	}
	if len(shortMessage) > 0 {
		c.AddTitle(shortMessage)
	}
	return c
}

type CFChatbotBlock struct {
	slack.Block
}

// slackBlocks converts the ChatMessage's blocks to a slice of slack.Blocks
func (c *ChatMessage) slackBlocks() []slack.Block {
	blocks := make([]slack.Block, len(c.Blocks))
	for i := range c.Blocks {
		blocks[i] = c.Blocks[i].Block
	}
	if len(c.AtMentionUser) > 0 {
		// We're supposed to be able to reply with a user ID - but we can't. So... we'll have to get the user info
		// userInfo, err := bot.SlackClient.GetUserInfo(c.AtMentionUser)
		// if err != nil {
		// 	bot.log.WithError(err).Error("Could not get user info")
		// } else {
		// 	bot.log.WithField("user", userInfo).Info("Found user info")
		// }
		// Prepend the @mention to the message
		c.Blocks = append([]CFChatbotBlock{
			{Block: slack.NewSectionBlock(
				slack.NewTextBlockObject(
					slack.MarkdownType,
					fmt.Sprintf("FYI <@%s>?", c.AtMentionUser),
					false, false,
				),
				nil,
				nil,
			),
			},
		}, c.Blocks...)
	}
	return blocks
}

// Send Posts the message to Slack and saves it to the DB.
// It returns an error if the message could not be sent or saved.
func (c *ChatMessage) Send() (msgID string, err error) {
	_, msgID, err = slackClient.PostMessage(c.ChannelName,
		slack.MsgOptionEnableLinkUnfurl(),
		slack.MsgOptionBlocks(
			c.slackBlocks()...,
		),
		slack.MsgOptionMetadata(slack.SlackMetadata{
			EventType:    "cf-emitting-chat-message",
			EventPayload: c.Metadata,
		}),
		slack.MsgOptionUsername(fmt.Sprintf("CF %s Chat Bot", "DEV")),
	)
	if err != nil {
		if IsChannelNotFound(err) {
			err = ErrChannelNotFound
		}
		return msgID, fmt.Errorf("could not send message to Slack channel '%s': %w", c.ChannelName, err)
	}
	return msgID, nil
}

// CompleteInteraction allows you to mark a slack interaction as "completed"
func CompleteInteraction(channelName, msgId, result, completedByUser string) (err error) {
	channelID, _, err := ChannelNameToID(channelName)
	if err != nil {
		return fmt.Errorf("unable to complete slack interaction out of band: %w", err)
	}
	text := fmt.Sprintf("Thank you for submitting your answer of '%s' %s! (We realize this is a thread - slack does *not* like updating the original message without a user initiated action).", result, completedByUser)
	// Note -  will post a thread reply
	_, _, _, err = slackClient.UpdateMessage(channelID,
		msgId,
		slack.MsgOptionText(text, false),
		slack.MsgOptionAsUser(false),
		slack.MsgOptionUsername(fmt.Sprintf("CF %s Chat Bot", consts.ActualCFEnv)))
	if err != nil {
		return fmt.Errorf("unable to complete slack interaction out of band: %w", err)
	}
	return nil
}

// ChannelNameToID converts a channel name to a channel ID - a note that this
// will call the Slack API to get the list of channels. Caching has been removed for this example.
func ChannelNameToID(channelName string) (channelID string, isChannelMember bool, err error) {
	// Remove the # if it's there
	channelName = strings.TrimPrefix(channelName, "#")

        // There was a cache here - removing to make it easier

	channelNamesToIDs, err := ListChannels()
	if err != nil {
		return "", false, err
	}
	channelID, ok := channelNamesToIDs[channelName]
	if !ok {
		return "", false, ErrChannelNotFound
	}
	return channelID, false, nil
}

// ListChannels returns a map of channel names to channel IDs for all public and
// private channels. These channels are not auto-joined. Caching has been removed for this example.
func ListChannels() (channelNamesToIDs map[string]string, err error) {
	channels, _, err := slackClient.GetConversations(&slack.GetConversationsParameters{
		ExcludeArchived: true,
		Limit:           1000,
		Types:           []string{"public_channel", "private_channel"},
	})
	if err != nil {
		return nil, err
	}
	channelNamesToIDs = make(map[string]string)
	for i := range channels {
		channelNamesToIDs[channels[i].Name] = channels[i].ID
	}
	return channelNamesToIDs, nil
}

// TestCompleteInteraction is a test which should first send a message, then
// subsequently update that message. We are able to get this test to sort of work
// by updating the slack.MsgOptionTS(msgId) - but that's not ideal as it doesn't allow us to
// mark the interaction as complete.
// We also can send using ephemeral messages, but that's not ideal either, as we want the message
// to persist until the interaction is either completed from with-in Slack or the
// external system completes the interaction.
func TestCompleteInteraction(t *testing.T) {
	is := assert.New(t)
	setup(t)
	msg := NewChatMessage("My initial message", "cf-chatbot-test-")
	// msg.AddControl("How are you?", formtypes.ControlTypeButton, "Good", "Bad")
	msgId, err := msg.Send()
	is.NoError(err, "Could not send chat message")
	is.NoError(CompleteInteraction(msg.ChannelName, msgId, "Good", "Topher Sterling"))
}

Note that changing the UpdateMessage code to use blocks rather than slack.MsgOptionText(text, false) fixes the issue and the update succeeds. If you swap the CompleteInteration/UpdateMessage call for this, then you'll see it work:

_, _, _, err = slackClient.UpdateMessage(channelID,
		msgId,
		slack.MsgOptionBlocks(slack.NewSectionBlock(
			slack.NewTextBlockObject(
				"mrkdwn", text, false, false),
			nil, nil)),
		slack.MsgOptionAsUser(false),
		slack.MsgOptionUsername(fmt.Sprintf("CF %s Chat Bot", consts.ActualCFEnv)))

So perhaps the SDK could convert the MsgOptionText to a single MsgOptionBlocks?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants