PLT-7077: ignore null array items in slack attachments (#6904)
* ignore null array items in incoming webhooks / command responses * consolidate code, process announcements in command response as well * make a bit more idiomatic, add tests * add missing file
Этот коммит содержится в:
коммит произвёл
Harrison Healey
родитель
83d53ea98c
Коммит
9ee7f661c7
@@ -5,7 +5,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ type CommandResponse struct {
|
|||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
IconURL string `json:"icon_url"`
|
IconURL string `json:"icon_url"`
|
||||||
GotoLocation string `json:"goto_location"`
|
GotoLocation string `json:"goto_location"`
|
||||||
Attachments []*SlackAttachment `json:"attachments"`
|
Attachments SlackAttachments `json:"attachments"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *CommandResponse) ToJson() string {
|
func (o *CommandResponse) ToJson() string {
|
||||||
@@ -40,14 +39,8 @@ func CommandResponseFromJson(data io.Reader) *CommandResponse {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure attachment fields are stored as strings
|
o.Text = ExpandAnnouncement(o.Text)
|
||||||
for _, attachment := range o.Attachments {
|
o.Attachments.Process()
|
||||||
for _, field := range attachment.Fields {
|
|
||||||
if field.Value != nil {
|
|
||||||
field.Value = fmt.Sprintf("%v", field.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &o
|
return &o
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,3 +82,17 @@ func TestCommandResponseFromJson(t *testing.T) {
|
|||||||
t.Fatal("should've received correct second attachment value")
|
t.Fatal("should've received correct second attachment value")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCommandResponseNullArrayItems(t *testing.T) {
|
||||||
|
payload := `{"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}, null]}, null]}`
|
||||||
|
cr := CommandResponseFromJson(strings.NewReader(payload))
|
||||||
|
if cr == nil {
|
||||||
|
t.Fatal("CommandResponse should not be nil")
|
||||||
|
}
|
||||||
|
if len(cr.Attachments) != 1 {
|
||||||
|
t.Fatalf("expected one attachment")
|
||||||
|
}
|
||||||
|
if len(cr.Attachments[0].Fields) != 1 {
|
||||||
|
t.Fatalf("expected one field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,10 +6,8 @@ package model
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -34,7 +32,7 @@ type IncomingWebhookRequest struct {
|
|||||||
IconURL string `json:"icon_url"`
|
IconURL string `json:"icon_url"`
|
||||||
ChannelName string `json:"channel"`
|
ChannelName string `json:"channel"`
|
||||||
Props StringInterface `json:"props"`
|
Props StringInterface `json:"props"`
|
||||||
Attachments []*SlackAttachment `json:"attachments"`
|
Attachments SlackAttachments `json:"attachments"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -193,39 +191,6 @@ func decodeIncomingWebhookRequest(by []byte) (*IncomingWebhookRequest, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To mention @channel via a webhook in Slack, the message should contain
|
|
||||||
// <!channel>, as explained at the bottom of this article:
|
|
||||||
// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements
|
|
||||||
func expandAnnouncement(text string) string {
|
|
||||||
c1 := "<!channel>"
|
|
||||||
c2 := "@channel"
|
|
||||||
if strings.Contains(text, c1) {
|
|
||||||
return strings.Replace(text, c1, c2, -1)
|
|
||||||
}
|
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expand announcements in incoming webhooks from Slack. Those announcements
|
|
||||||
// can be found in the text attribute, or in the pretext, text, title and value
|
|
||||||
// attributes of the attachment structure. The Slack attachment structure is
|
|
||||||
// documented here: https://api.slack.com/docs/attachments
|
|
||||||
func expandAnnouncements(i *IncomingWebhookRequest) {
|
|
||||||
i.Text = expandAnnouncement(i.Text)
|
|
||||||
|
|
||||||
for _, attachment := range i.Attachments {
|
|
||||||
attachment.Pretext = expandAnnouncement(attachment.Pretext)
|
|
||||||
attachment.Text = expandAnnouncement(attachment.Text)
|
|
||||||
attachment.Title = expandAnnouncement(attachment.Title)
|
|
||||||
|
|
||||||
for _, field := range attachment.Fields {
|
|
||||||
if field.Value != nil {
|
|
||||||
// Ensure the value is set to a string if it is set
|
|
||||||
field.Value = expandAnnouncement(fmt.Sprintf("%v", field.Value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
|
func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
buf.ReadFrom(data)
|
buf.ReadFrom(data)
|
||||||
@@ -241,7 +206,8 @@ func IncomingWebhookRequestFromJson(data io.Reader) *IncomingWebhookRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
expandAnnouncements(o)
|
o.Text = ExpandAnnouncement(o.Text)
|
||||||
|
o.Attachments.Process()
|
||||||
|
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,3 +230,17 @@ func TestIncomingWebhookRequestFromJson(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIncomingWebhookNullArrayItems(t *testing.T) {
|
||||||
|
payload := `{"attachments":[{"fields":[{"title":"foo","value":"bar","short":true}, null]}, null]}`
|
||||||
|
iwr := IncomingWebhookRequestFromJson(strings.NewReader(payload))
|
||||||
|
if iwr == nil {
|
||||||
|
t.Fatal("IncomingWebhookRequest should not be nil")
|
||||||
|
}
|
||||||
|
if len(iwr.Attachments) != 1 {
|
||||||
|
t.Fatalf("expected one attachment")
|
||||||
|
}
|
||||||
|
if len(iwr.Attachments[0].Fields) != 1 {
|
||||||
|
t.Fatalf("expected one field")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,11 @@
|
|||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
type SlackAttachment struct {
|
type SlackAttachment struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Fallback string `json:"fallback"`
|
Fallback string `json:"fallback"`
|
||||||
@@ -27,3 +32,50 @@ type SlackAttachmentField struct {
|
|||||||
Value interface{} `json:"value"`
|
Value interface{} `json:"value"`
|
||||||
Short bool `json:"short"`
|
Short bool `json:"short"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SlackAttachments []*SlackAttachment
|
||||||
|
|
||||||
|
// To mention @channel via a webhook in Slack, the message should contain
|
||||||
|
// <!channel>, as explained at the bottom of this article:
|
||||||
|
// https://get.slack.help/hc/en-us/articles/202009646-Making-announcements
|
||||||
|
func ExpandAnnouncement(text string) string {
|
||||||
|
c1 := "<!channel>"
|
||||||
|
c2 := "@channel"
|
||||||
|
if strings.Contains(text, c1) {
|
||||||
|
return strings.Replace(text, c1, c2, -1)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand announcements in incoming webhooks from Slack. Those announcements
|
||||||
|
// can be found in the text attribute, or in the pretext, text, title and value
|
||||||
|
// attributes of the attachment structure. The Slack attachment structure is
|
||||||
|
// documented here: https://api.slack.com/docs/attachments
|
||||||
|
func (a *SlackAttachments) Process() {
|
||||||
|
var nonNilAttachments []*SlackAttachment
|
||||||
|
for _, attachment := range *a {
|
||||||
|
if attachment == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nonNilAttachments = append(nonNilAttachments, attachment)
|
||||||
|
|
||||||
|
attachment.Pretext = ExpandAnnouncement(attachment.Pretext)
|
||||||
|
attachment.Text = ExpandAnnouncement(attachment.Text)
|
||||||
|
attachment.Title = ExpandAnnouncement(attachment.Title)
|
||||||
|
|
||||||
|
var nonNilFields []*SlackAttachmentField
|
||||||
|
for _, field := range attachment.Fields {
|
||||||
|
if field == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
nonNilFields = append(nonNilFields, field)
|
||||||
|
|
||||||
|
if field.Value != nil {
|
||||||
|
// Ensure the value is set to a string if it is set
|
||||||
|
field.Value = ExpandAnnouncement(fmt.Sprintf("%v", field.Value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attachment.Fields = nonNilFields
|
||||||
|
}
|
||||||
|
*a = nonNilAttachments
|
||||||
|
}
|
||||||
|
|||||||
38
model/slack_attachment_test.go
Обычный файл
38
model/slack_attachment_test.go
Обычный файл
@@ -0,0 +1,38 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExpandAnnouncement(t *testing.T) {
|
||||||
|
if ExpandAnnouncement("<!channel> foo <!channel>") != "@channel foo @channel" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSlackAnnouncementProcess(t *testing.T) {
|
||||||
|
attachments := SlackAttachments{
|
||||||
|
{
|
||||||
|
Pretext: "<!channel> pretext",
|
||||||
|
Text: "<!channel> text",
|
||||||
|
Title: "<!channel> title",
|
||||||
|
Fields: []*SlackAttachmentField{
|
||||||
|
{
|
||||||
|
Title: "foo",
|
||||||
|
Value: "<!channel> bar",
|
||||||
|
Short: true,
|
||||||
|
}, nil,
|
||||||
|
},
|
||||||
|
}, nil,
|
||||||
|
}
|
||||||
|
attachments.Process()
|
||||||
|
if len(attachments) != 1 || len(attachments[0].Fields) != 1 {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if attachments[0].Pretext != "@channel pretext" ||
|
||||||
|
attachments[0].Text != "@channel text" ||
|
||||||
|
attachments[0].Title != "@channel title" ||
|
||||||
|
attachments[0].Fields[0].Value != "@channel bar" {
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
Ссылка в новой задаче
Block a user