Open batazor opened 1 year ago
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
?
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 messageExpected behavior
The
?blocks=[]
field is added to the querySteps to reproduce
reproducible code
manifest.yaml
Versions