From 67c68dbd6a2462a8af773a1520c7ca0c64d76072 Mon Sep 17 00:00:00 2001 From: Aviral Dasgupta Date: Sat, 10 Sep 2016 21:03:00 +0530 Subject: [PATCH] slackapi enhancements, use html/template properly --- .../go-neb/services/slackapi/message.go | 204 +++++++++++------- .../go-neb/services/slackapi/slackapi.go | 21 +- 2 files changed, 141 insertions(+), 84 deletions(-) diff --git a/src/github.com/matrix-org/go-neb/services/slackapi/message.go b/src/github.com/matrix-org/go-neb/services/slackapi/message.go index 1c2df8e..2c5ecd4 100644 --- a/src/github.com/matrix-org/go-neb/services/slackapi/message.go +++ b/src/github.com/matrix-org/go-neb/services/slackapi/message.go @@ -8,37 +8,44 @@ import ( log "github.com/Sirupsen/logrus" "github.com/matrix-org/go-neb/matrix" "github.com/russross/blackfriday" + "html/template" "io/ioutil" "mime" "net/http" "regexp" - "text/template" "time" ) type slackAttachment struct { - Fallback string `json:"fallback"` - Color *string `json:"color"` - Pretext string `json:"pretext"` - - AuthorName *string `json:"author_name"` - AuthorLink *string `json:"author_link"` - AuthorIcon *string `json:"author_icon"` + Fallback string `json:"fallback"` + FallbackRendered template.HTML + Color *string `json:"color"` + ColorRendered template.HTMLAttr + Pretext string `json:"pretext"` + PretextRendered template.HTML + + AuthorName *string `json:"author_name"` + AuthorLink template.URL `json:"author_link"` + AuthorIcon *string `json:"author_icon"` + AuthorIconURL template.URL Title *string `json:"title"` TitleLink *string `json:"title_link"` - Text string `json:"text"` + Text string `json:"text"` + TextRendered template.HTML + MrkdwnIn []string `json:"mrkdwn_in"` Ts *int64 `json:"ts"` } type slackMessage struct { - Text string `json:"text"` - Username string `json:"username"` - Channel string `json:"channel"` - Mrkdwn *bool `json:"mrkdwn"` - Attachments []slackAttachment `json:"attachments"` + Text string `json:"text"` + TextRendered template.HTML + Username string `json:"username"` + Channel string `json:"channel"` + Mrkdwn *bool `json:"mrkdwn"` + Attachments []slackAttachment `json:"attachments"` } // We use text.template because any fields of any attachments could @@ -46,19 +53,31 @@ type slackMessage struct { // We do not do this yet, since it's assumed that clients also escape the content we send them. var htmlTemplate, _ = template.New("htmlTemplate").Parse(` @{{ .Username }} via #{{ .Channel }}
-{{ if .Text }}{{ .Text }}
{{ end }} -{{ range .Attachments }} - {{ if .AuthorName }} - {{if .AuthorLink }}{{ end }} - {{ if .AuthorIcon }}{{ .AuthorIcon }}{{ end }} - {{ .AuthorName }} - {{if .AuthorLink }}{{ end }} +{{- with (or .TextRendered .Text nil) }} + {{- if . }} + {{- . }}
+ {{- end }} +{{- end }} +{{- range .Attachments }} + {{- if .AuthorName }} + {{- if .AuthorLink }}{{ end }} + {{- if .AuthorIconUrl }}{{ end }} + {{- .AuthorName }} + {{- if .AuthorLink }}{{ end }}
- {{ end }} - {{ if .TitleLink }}{{ .Title }}{{ else }}{{ .Title }}{{ end }}
- {{ if .Pretext }}{{ .Pretext }}
{{ end }} - {{ if .Text }}{{ .Text }}
{{ end }} -{{ end }} + {{- end }} + + + {{- if .TitleLink }} + {{ .Title }} + {{- else }} + {{- .Title }} + {{- end }} +
+
+ {{- if .Pretext }}{{ or .PretextRendered .Pretext }}
{{ end }} + {{- if .Text }}{{ or .TextRendered .Text }}
{{ end }} +{{- end }} `) var netClient = &http.Client{ @@ -80,7 +99,7 @@ func getSlackMessage(req http.Request) (message slackMessage, err error) { decoder := json.NewDecoder(req.Body) err = decoder.Decode(&message) } else { - message.Text = fmt.Sprint("**Error:** unknown Content-Type `%s`", ct) + message.Text = fmt.Sprintf("**Error:** unknown Content-Type `%s`", ct) log.Error(message.Text) } @@ -92,76 +111,99 @@ func linkifyString(text string) string { } func getColor(color *string) string { - if color != nil { - // https://api.slack.com/docs/message-attachments defines these aliases - mappedColor, ok := map[string]string{ - "good": "green", - "warning": "yellow", - "danger": "red", - }[*color] - if ok { - return mappedColor - } - return *color + if color == nil { + return "black" } - return "black" + + // https://api.slack.com/docs/message-attachments defines these aliases + mappedColor, ok := map[string]string{ + "good": "green", + "warning": "yellow", + "danger": "red", + }[*color] + if ok { + return mappedColor + } + return *color } -func slackMessageToHTMLMessage(message slackMessage) (html matrix.HTMLMessage, err error) { - processedMessage := message +// fetches an image and encodes it as a data URL +// returns nil if fetch fails +func fetchAndEncodeImage(url *string) (data template.URL) { + if url == nil { + return + } - if message.Mrkdwn == nil || *message.Mrkdwn == true { - text := linkifyString(message.Text) + var resp *http.Response + resp, err := netClient.Get(*url) + if err == nil { + var ( + body []byte + contentType string + ) + + if body, err = ioutil.ReadAll(resp.Body); err != nil { + return + } + if contentType, _, err = mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil { + return + } + base64Body := base64.StdEncoding.EncodeToString(body) + data = template.URL(fmt.Sprintf("data:%s;base64,%s", contentType, base64Body)) + } + + return +} - processedMessage.Text = string(blackfriday.MarkdownBasic([]byte(text))) +func renderSlackAttachment(attachment *slackAttachment) { + if attachment == nil { + return } - for attachmentID, attachment := range message.Attachments { - target := &processedMessage.Attachments[attachmentID] - - color := getColor(attachment.Color) - target.Color = &color - - if attachment.AuthorIcon != nil { - var resp *http.Response - resp, err = netClient.Get(*attachment.AuthorIcon) - if err == nil { - body, _ := ioutil.ReadAll(resp.Body) - ct, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) - b64body := base64.StdEncoding.EncodeToString(body) - *target.AuthorIcon = fmt.Sprintf("", ct, b64body) - } else { - *target.AuthorIcon = "" - } + attachment.ColorRendered = template.HTMLAttr(getColor(attachment.Color)) + attachment.AuthorIconURL = fetchAndEncodeImage(attachment.AuthorIcon) + + for _, fieldName := range attachment.MrkdwnIn { + var ( + srcField *string + targetField *template.HTML + ) + + switch fieldName { + case "text": + srcField = &attachment.Text + targetField = &attachment.TextRendered + case "pretext": + srcField = &attachment.Pretext + targetField = &attachment.PretextRendered + case "fallback": + srcField = &attachment.Fallback + targetField = &attachment.FallbackRendered } - for _, fieldName := range attachment.MrkdwnIn { - var targetField, srcField *string - - switch fieldName { - case "text": - srcField = &attachment.Text - targetField = &target.Text - break - case "pretext": - srcField = &attachment.Pretext - targetField = &target.Pretext - break - } - - if targetField != nil && srcField != nil { - value := string( - blackfriday.MarkdownBasic([]byte(linkifyString(*srcField)))) - targetField = &value - } + if targetField != nil && srcField != nil { + log.Info(targetField) + *targetField = template.HTML( + blackfriday.MarkdownBasic([]byte(linkifyString(*srcField)))) } } +} + +func slackMessageToHTMLMessage(message slackMessage) (html matrix.HTMLMessage, err error) { + text := linkifyString(message.Text) + if message.Mrkdwn == nil || *message.Mrkdwn == true { + message.TextRendered = template.HTML(blackfriday.MarkdownBasic([]byte(text))) + } + + for attachmentID := range message.Attachments { + renderSlackAttachment(&message.Attachments[attachmentID]) + } var buffer bytes.Buffer html.MsgType = "m.text" html.Format = "org.matrix.custom.html" html.Body, _ = slackMessageToMarkdown(message) - err = htmlTemplate.ExecuteTemplate(&buffer, "htmlTemplate", processedMessage) + err = htmlTemplate.ExecuteTemplate(&buffer, "htmlTemplate", message) html.FormattedBody = buffer.String() return } diff --git a/src/github.com/matrix-org/go-neb/services/slackapi/slackapi.go b/src/github.com/matrix-org/go-neb/services/slackapi/slackapi.go index 376d729..2c34c8e 100644 --- a/src/github.com/matrix-org/go-neb/services/slackapi/slackapi.go +++ b/src/github.com/matrix-org/go-neb/services/slackapi/slackapi.go @@ -13,7 +13,11 @@ type slackAPIService struct { serviceUserID string webhookEndpointURL string ClientUserID string - Hooks map[string]string + // maps from hookID -> roomID + Hooks map[string]struct { + RoomID string + MessageType string + } } func (s *slackAPIService) ServiceUserID() string { return s.serviceUserID } @@ -30,15 +34,26 @@ func (s *slackAPIService) Plugin(cli *matrix.Client, roomID string) plugin.Plugi func (s *slackAPIService) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { segments := strings.Split(req.URL.Path, "/") + + if len(segments) < 2 { + w.WriteHeader(400) + } + hookID := segments[len(segments)-2] - messageType := segments[len(segments)-3] - roomID := s.Hooks[hookID] + messageType := s.Hooks[hookID].MessageType + if messageType == "" { + messageType = "m.text" + } + roomID := s.Hooks[hookID].RoomID slackMessage, err := getSlackMessage(*req) if err != nil { return } htmlMessage, err := slackMessageToHTMLMessage(slackMessage) + if err != nil { + return + } htmlMessage.MsgType = messageType cli.SendMessageEvent( roomID,