package slackapi
import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"io/ioutil"
"mime"
"net/http"
"regexp"
"time"
"github.com/russross/blackfriday"
log "github.com/sirupsen/logrus"
mevt "maunium.net/go/mautrix/event"
)
type slackAttachment struct {
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"`
TextRendered template.HTML
MrkdwnIn []string `json:"mrkdwn_in"`
Ts *int64 `json:"ts"`
}
type slackMessage struct {
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
// be Markdown, so it's convenient to escape on a field-by field basis.
// 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 }}
{{- 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 }}{{ or .PretextRendered .Pretext }}
{{ end }}
{{- if .Text }}{{ or .TextRendered .Text }}
{{ end }}
{{- end }}
`)
var netClient = &http.Client{
Timeout: time.Second * 10,
}
// TODO: What does this do?
var linkRegex, _ = regexp.Compile("<([^|]+)(\\|([^>]+))?>")
func getSlackMessage(req http.Request) (message slackMessage, err error) {
ct := req.Header.Get("Content-Type")
ct, _, err = mime.ParseMediaType(ct)
if ct == "application/x-www-form-urlencoded" {
req.ParseForm()
payload := req.Form.Get("payload")
err = json.Unmarshal([]byte(payload), &message)
} else if ct == "application/json" {
decoder := json.NewDecoder(req.Body)
err = decoder.Decode(&message)
} else {
message.Text = fmt.Sprintf("**Error:** unknown Content-Type `%s`", ct)
log.Error(message.Text)
}
return
}
func linkifyString(text string) string {
return linkRegex.ReplaceAllString(text, "$3")
}
// Convert a Slack colour (defined at https://api.slack.com/docs/message-attachments )
// into an HTML color.
func getColor(color *string) string {
if color == nil {
return "black"
}
mappedColor, ok := map[string]string{
"good": "green",
"warning": "yellow",
"danger": "red",
}[*color]
if ok {
return mappedColor
}
// HTML color= attributes support any arbitrary string, so just pass through.
return *color
}
// fetches an image and encodes it as a data URL
// returns an empty string if fetch fails
func fetchAndEncodeImage(url *string) (data template.URL) {
if url == nil {
return
}
var resp *http.Response
resp, err := netClient.Get(*url)
if err != nil {
log.WithError(err).WithField("url", url).Error("Failed to GET URL")
return
}
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
}
func renderSlackAttachment(attachment *slackAttachment) {
if attachment == nil {
return
}
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
}
if targetField != nil && srcField != nil {
*targetField = template.HTML(
blackfriday.MarkdownBasic([]byte(linkifyString(*srcField))))
}
}
}
func slackMessageToHTMLMessage(message slackMessage) (html mevt.MessageEventContent, 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", message)
html.FormattedBody = buffer.String()
return
}
// This can be improved; Markdown does support all of Slack's formatting
// Which we're just throwing away at the moment.
func slackMessageToMarkdown(message slackMessage) (markdown string, err error) {
markdown += message.Text + "\n"
for _, attachment := range message.Attachments {
markdown += attachment.Fallback + "\n"
}
return
}