You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

225 lines
5.9 KiB

  1. package slackapi
  2. import (
  3. "bytes"
  4. "encoding/base64"
  5. "encoding/json"
  6. "fmt"
  7. "html/template"
  8. "io/ioutil"
  9. "mime"
  10. "net/http"
  11. "regexp"
  12. "time"
  13. "github.com/russross/blackfriday"
  14. log "github.com/sirupsen/logrus"
  15. mevt "maunium.net/go/mautrix/event"
  16. )
  17. type slackAttachment struct {
  18. Fallback string `json:"fallback"`
  19. FallbackRendered template.HTML
  20. Color *string `json:"color"`
  21. ColorRendered template.HTMLAttr
  22. Pretext string `json:"pretext"`
  23. PretextRendered template.HTML
  24. AuthorName *string `json:"author_name"`
  25. AuthorLink template.URL `json:"author_link"`
  26. AuthorIcon *string `json:"author_icon"`
  27. AuthorIconURL template.URL
  28. Title *string `json:"title"`
  29. TitleLink *string `json:"title_link"`
  30. Text string `json:"text"`
  31. TextRendered template.HTML
  32. MrkdwnIn []string `json:"mrkdwn_in"`
  33. TS *int64 `json:"ts"`
  34. }
  35. type slackMessage struct {
  36. Text string `json:"text"`
  37. TextRendered template.HTML
  38. Username string `json:"username"`
  39. Channel string `json:"channel"`
  40. Mrkdwn *bool `json:"mrkdwn"`
  41. Attachments []slackAttachment `json:"attachments"`
  42. }
  43. // We use text.template because any fields of any attachments could
  44. // be Markdown, so it's convenient to escape on a field-by field basis.
  45. // We do not do this yet, since it's assumed that clients also escape the content we send them.
  46. var htmlTemplate, _ = template.New("htmlTemplate").Parse(`
  47. <strong>@{{ .Username }}</strong> via <strong>#{{ .Channel }}</strong><br />
  48. {{- with (or .TextRendered .Text nil) }}
  49. {{- if . }}
  50. {{- . }}<br />
  51. {{- end }}
  52. {{- end }}
  53. {{- range .Attachments }}
  54. {{- if .AuthorName }}
  55. {{- if .AuthorLink }}<a href="{{ .AuthorLink }}">{{ end }}
  56. {{- if .AuthorIconUrl }}<img src="{{ .AuthorIconUrl }}" />{{ end }}
  57. {{- .AuthorName }}
  58. {{- if .AuthorLink }}</a>{{ end }}
  59. <br />
  60. {{- end }}
  61. <strong>
  62. <font color="{{- .ColorRendered }}"></font>
  63. {{- if .TitleLink }}
  64. <a href="{{ .TitleLink}}">{{ .Title }}</a>
  65. {{- else }}
  66. {{- .Title }}
  67. {{- end }}
  68. <br />
  69. </strong>
  70. {{- if .Pretext }}{{ or .PretextRendered .Pretext }}<br />{{ end }}
  71. {{- if .Text }}{{ or .TextRendered .Text }}<br />{{ end }}
  72. {{- end }}
  73. `)
  74. var netClient = &http.Client{
  75. Timeout: time.Second * 10,
  76. }
  77. // TODO: What does this do?
  78. var linkRegex, _ = regexp.Compile(`<([^|]+)(\|([^>]+))?>`)
  79. func getSlackMessage(req http.Request) (message slackMessage, err error) {
  80. ct := req.Header.Get("Content-Type")
  81. ct, _, err = mime.ParseMediaType(ct)
  82. if ct == "application/x-www-form-urlencoded" {
  83. req.ParseForm()
  84. payload := req.Form.Get("payload")
  85. err = json.Unmarshal([]byte(payload), &message)
  86. } else if ct == "application/json" {
  87. decoder := json.NewDecoder(req.Body)
  88. err = decoder.Decode(&message)
  89. } else {
  90. message.Text = fmt.Sprintf("**Error:** unknown Content-Type `%s`", ct)
  91. log.Error(message.Text)
  92. }
  93. return
  94. }
  95. func linkifyString(text string) string {
  96. return linkRegex.ReplaceAllString(text, "<a href=\"$1\">$3</a>")
  97. }
  98. // Convert a Slack colour (defined at https://api.slack.com/docs/message-attachments )
  99. // into an HTML color.
  100. func getColor(color *string) string {
  101. if color == nil {
  102. return "black"
  103. }
  104. mappedColor, ok := map[string]string{
  105. "good": "green",
  106. "warning": "yellow",
  107. "danger": "red",
  108. }[*color]
  109. if ok {
  110. return mappedColor
  111. }
  112. // HTML color= attributes support any arbitrary string, so just pass through.
  113. return *color
  114. }
  115. // fetches an image and encodes it as a data URL
  116. // returns an empty string if fetch fails
  117. func fetchAndEncodeImage(url *string) (data template.URL) {
  118. if url == nil {
  119. return
  120. }
  121. var resp *http.Response
  122. resp, err := netClient.Get(*url)
  123. if err != nil {
  124. log.WithError(err).WithField("url", url).Error("Failed to GET URL")
  125. return
  126. }
  127. var (
  128. body []byte
  129. contentType string
  130. )
  131. if body, err = ioutil.ReadAll(resp.Body); err != nil {
  132. return
  133. }
  134. if contentType, _, err = mime.ParseMediaType(resp.Header.Get("Content-Type")); err != nil {
  135. return
  136. }
  137. base64Body := base64.StdEncoding.EncodeToString(body)
  138. data = template.URL(fmt.Sprintf("data:%s;base64,%s", contentType, base64Body))
  139. return
  140. }
  141. func renderSlackAttachment(attachment *slackAttachment) {
  142. if attachment == nil {
  143. return
  144. }
  145. attachment.ColorRendered = template.HTMLAttr(getColor(attachment.Color))
  146. attachment.AuthorIconURL = fetchAndEncodeImage(attachment.AuthorIcon)
  147. for _, fieldName := range attachment.MrkdwnIn {
  148. var (
  149. srcField *string
  150. targetField *template.HTML
  151. )
  152. switch fieldName {
  153. case "text":
  154. srcField = &attachment.Text
  155. targetField = &attachment.TextRendered
  156. case "pretext":
  157. srcField = &attachment.Pretext
  158. targetField = &attachment.PretextRendered
  159. case "fallback":
  160. srcField = &attachment.Fallback
  161. targetField = &attachment.FallbackRendered
  162. }
  163. if targetField != nil && srcField != nil {
  164. *targetField = template.HTML(
  165. blackfriday.MarkdownBasic([]byte(linkifyString(*srcField))))
  166. }
  167. }
  168. }
  169. func slackMessageToHTMLMessage(message slackMessage) (html mevt.MessageEventContent, err error) {
  170. text := linkifyString(message.Text)
  171. if message.Mrkdwn == nil || *message.Mrkdwn {
  172. message.TextRendered = template.HTML(blackfriday.MarkdownBasic([]byte(text)))
  173. }
  174. for attachmentID := range message.Attachments {
  175. renderSlackAttachment(&message.Attachments[attachmentID])
  176. }
  177. var buffer bytes.Buffer
  178. html.MsgType = "m.text"
  179. html.Format = "org.matrix.custom.html"
  180. html.Body, _ = slackMessageToMarkdown(message)
  181. err = htmlTemplate.ExecuteTemplate(&buffer, "htmlTemplate", message)
  182. html.FormattedBody = buffer.String()
  183. return
  184. }
  185. // This can be improved; Markdown does support all of Slack's formatting
  186. // Which we're just throwing away at the moment.
  187. func slackMessageToMarkdown(message slackMessage) (markdown string, err error) {
  188. markdown += message.Text + "\n"
  189. for _, attachment := range message.Attachments {
  190. markdown += attachment.Fallback + "\n"
  191. }
  192. return
  193. }