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.

423 lines
15 KiB

9 years ago
9 years ago
9 years ago
  1. // Copyright 2014 The Gogs Authors. All rights reserved.
  2. // Use of this source code is governed by a MIT-style
  3. // license that can be found in the LICENSE file.
  4. package models
  5. import (
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "strings"
  10. "code.gitea.io/gitea/modules/git"
  11. "code.gitea.io/gitea/modules/setting"
  12. api "code.gitea.io/sdk/gitea"
  13. )
  14. // SlackMeta contains the slack metadata
  15. type SlackMeta struct {
  16. Channel string `json:"channel"`
  17. Username string `json:"username"`
  18. IconURL string `json:"icon_url"`
  19. Color string `json:"color"`
  20. }
  21. // SlackPayload contains the information about the slack channel
  22. type SlackPayload struct {
  23. Channel string `json:"channel"`
  24. Text string `json:"text"`
  25. Username string `json:"username"`
  26. IconURL string `json:"icon_url"`
  27. UnfurlLinks int `json:"unfurl_links"`
  28. LinkNames int `json:"link_names"`
  29. Attachments []SlackAttachment `json:"attachments"`
  30. }
  31. // SlackAttachment contains the slack message
  32. type SlackAttachment struct {
  33. Fallback string `json:"fallback"`
  34. Color string `json:"color"`
  35. Title string `json:"title"`
  36. Text string `json:"text"`
  37. }
  38. // SetSecret sets the slack secret
  39. func (p *SlackPayload) SetSecret(_ string) {}
  40. // JSONPayload Marshals the SlackPayload to json
  41. func (p *SlackPayload) JSONPayload() ([]byte, error) {
  42. data, err := json.MarshalIndent(p, "", " ")
  43. if err != nil {
  44. return []byte{}, err
  45. }
  46. return data, nil
  47. }
  48. // SlackTextFormatter replaces &, <, > with HTML characters
  49. // see: https://api.slack.com/docs/formatting
  50. func SlackTextFormatter(s string) string {
  51. // replace & < >
  52. s = strings.Replace(s, "&", "&amp;", -1)
  53. s = strings.Replace(s, "<", "&lt;", -1)
  54. s = strings.Replace(s, ">", "&gt;", -1)
  55. return s
  56. }
  57. // SlackShortTextFormatter replaces &, <, > with HTML characters
  58. func SlackShortTextFormatter(s string) string {
  59. s = strings.Split(s, "\n")[0]
  60. // replace & < >
  61. s = strings.Replace(s, "&", "&amp;", -1)
  62. s = strings.Replace(s, "<", "&lt;", -1)
  63. s = strings.Replace(s, ">", "&gt;", -1)
  64. return s
  65. }
  66. // SlackLinkFormatter creates a link compatible with slack
  67. func SlackLinkFormatter(url string, text string) string {
  68. return fmt.Sprintf("<%s|%s>", url, SlackTextFormatter(text))
  69. }
  70. // SlackLinkToRef slack-formatter link to a repo ref
  71. func SlackLinkToRef(repoURL, ref string) string {
  72. refName := git.RefEndName(ref)
  73. switch {
  74. case strings.HasPrefix(ref, git.BranchPrefix):
  75. return SlackLinkFormatter(repoURL+"/src/branch/"+refName, refName)
  76. case strings.HasPrefix(ref, git.TagPrefix):
  77. return SlackLinkFormatter(repoURL+"/src/tag/"+refName, refName)
  78. default:
  79. return SlackLinkFormatter(repoURL+"/src/commit/"+refName, refName)
  80. }
  81. }
  82. func getSlackCreatePayload(p *api.CreatePayload, slack *SlackMeta) (*SlackPayload, error) {
  83. repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  84. refLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
  85. text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
  86. return &SlackPayload{
  87. Channel: slack.Channel,
  88. Text: text,
  89. Username: slack.Username,
  90. IconURL: slack.IconURL,
  91. }, nil
  92. }
  93. // getSlackDeletePayload composes Slack payload for delete a branch or tag.
  94. func getSlackDeletePayload(p *api.DeletePayload, slack *SlackMeta) (*SlackPayload, error) {
  95. refName := git.RefEndName(p.Ref)
  96. repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  97. text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
  98. return &SlackPayload{
  99. Channel: slack.Channel,
  100. Text: text,
  101. Username: slack.Username,
  102. IconURL: slack.IconURL,
  103. }, nil
  104. }
  105. // getSlackForkPayload composes Slack payload for forked by a repository.
  106. func getSlackForkPayload(p *api.ForkPayload, slack *SlackMeta) (*SlackPayload, error) {
  107. baseLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  108. forkLink := SlackLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
  109. text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
  110. return &SlackPayload{
  111. Channel: slack.Channel,
  112. Text: text,
  113. Username: slack.Username,
  114. IconURL: slack.IconURL,
  115. }, nil
  116. }
  117. func getSlackIssuesPayload(p *api.IssuePayload, slack *SlackMeta) (*SlackPayload, error) {
  118. senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
  119. titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index),
  120. fmt.Sprintf("#%d %s", p.Index, p.Issue.Title))
  121. var text, title, attachmentText string
  122. switch p.Action {
  123. case api.HookIssueOpened:
  124. text = fmt.Sprintf("[%s] Issue submitted by %s", p.Repository.FullName, senderLink)
  125. title = titleLink
  126. attachmentText = SlackTextFormatter(p.Issue.Body)
  127. case api.HookIssueClosed:
  128. text = fmt.Sprintf("[%s] Issue closed: %s by %s", p.Repository.FullName, titleLink, senderLink)
  129. case api.HookIssueReOpened:
  130. text = fmt.Sprintf("[%s] Issue re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink)
  131. case api.HookIssueEdited:
  132. text = fmt.Sprintf("[%s] Issue edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
  133. attachmentText = SlackTextFormatter(p.Issue.Body)
  134. case api.HookIssueAssigned:
  135. text = fmt.Sprintf("[%s] Issue assigned to %s: %s by %s", p.Repository.FullName,
  136. SlackLinkFormatter(setting.AppURL+p.Issue.Assignee.UserName, p.Issue.Assignee.UserName),
  137. titleLink, senderLink)
  138. case api.HookIssueUnassigned:
  139. text = fmt.Sprintf("[%s] Issue unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)
  140. case api.HookIssueLabelUpdated:
  141. text = fmt.Sprintf("[%s] Issue labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink)
  142. case api.HookIssueLabelCleared:
  143. text = fmt.Sprintf("[%s] Issue labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink)
  144. case api.HookIssueSynchronized:
  145. text = fmt.Sprintf("[%s] Issue synchronized: %s by %s", p.Repository.FullName, titleLink, senderLink)
  146. case api.HookIssueMilestoned:
  147. text = fmt.Sprintf("[%s] Issue milestoned: #%s %s", p.Repository.FullName, titleLink, senderLink)
  148. case api.HookIssueDemilestoned:
  149. text = fmt.Sprintf("[%s] Issue milestone cleared: #%s %s", p.Repository.FullName, titleLink, senderLink)
  150. }
  151. return &SlackPayload{
  152. Channel: slack.Channel,
  153. Text: text,
  154. Username: slack.Username,
  155. IconURL: slack.IconURL,
  156. Attachments: []SlackAttachment{{
  157. Color: slack.Color,
  158. Title: title,
  159. Text: attachmentText,
  160. }},
  161. }, nil
  162. }
  163. func getSlackIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*SlackPayload, error) {
  164. senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
  165. titleLink := SlackLinkFormatter(fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID)),
  166. fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title))
  167. var text, title, attachmentText string
  168. switch p.Action {
  169. case api.HookIssueCommentCreated:
  170. text = fmt.Sprintf("[%s] New comment created by %s", p.Repository.FullName, senderLink)
  171. title = titleLink
  172. attachmentText = SlackTextFormatter(p.Comment.Body)
  173. case api.HookIssueCommentEdited:
  174. text = fmt.Sprintf("[%s] Comment edited by %s", p.Repository.FullName, senderLink)
  175. title = titleLink
  176. attachmentText = SlackTextFormatter(p.Comment.Body)
  177. case api.HookIssueCommentDeleted:
  178. text = fmt.Sprintf("[%s] Comment deleted by %s", p.Repository.FullName, senderLink)
  179. title = SlackLinkFormatter(fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index),
  180. fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title))
  181. attachmentText = SlackTextFormatter(p.Comment.Body)
  182. }
  183. return &SlackPayload{
  184. Channel: slack.Channel,
  185. Text: text,
  186. Username: slack.Username,
  187. IconURL: slack.IconURL,
  188. Attachments: []SlackAttachment{{
  189. Color: slack.Color,
  190. Title: title,
  191. Text: attachmentText,
  192. }},
  193. }, nil
  194. }
  195. func getSlackReleasePayload(p *api.ReleasePayload, slack *SlackMeta) (*SlackPayload, error) {
  196. repoLink := SlackLinkFormatter(p.Repository.HTMLURL, p.Repository.Name)
  197. refLink := SlackLinkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName)
  198. var text string
  199. switch p.Action {
  200. case api.HookReleasePublished:
  201. text = fmt.Sprintf("[%s] new release %s published by %s", repoLink, refLink, p.Sender.UserName)
  202. case api.HookReleaseUpdated:
  203. text = fmt.Sprintf("[%s] new release %s updated by %s", repoLink, refLink, p.Sender.UserName)
  204. case api.HookReleaseDeleted:
  205. text = fmt.Sprintf("[%s] new release %s deleted by %s", repoLink, refLink, p.Sender.UserName)
  206. }
  207. return &SlackPayload{
  208. Channel: slack.Channel,
  209. Text: text,
  210. Username: slack.Username,
  211. IconURL: slack.IconURL,
  212. }, nil
  213. }
  214. func getSlackPushPayload(p *api.PushPayload, slack *SlackMeta) (*SlackPayload, error) {
  215. // n new commits
  216. var (
  217. commitDesc string
  218. commitString string
  219. )
  220. if len(p.Commits) == 1 {
  221. commitDesc = "1 new commit"
  222. } else {
  223. commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
  224. }
  225. if len(p.CompareURL) > 0 {
  226. commitString = SlackLinkFormatter(p.CompareURL, commitDesc)
  227. } else {
  228. commitString = commitDesc
  229. }
  230. repoLink := SlackLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  231. branchLink := SlackLinkToRef(p.Repo.HTMLURL, p.Ref)
  232. text := fmt.Sprintf("[%s:%s] %s pushed by %s", repoLink, branchLink, commitString, p.Pusher.UserName)
  233. var attachmentText string
  234. // for each commit, generate attachment text
  235. for i, commit := range p.Commits {
  236. attachmentText += fmt.Sprintf("%s: %s - %s", SlackLinkFormatter(commit.URL, commit.ID[:7]), SlackShortTextFormatter(commit.Message), SlackTextFormatter(commit.Author.Name))
  237. // add linebreak to each commit but the last
  238. if i < len(p.Commits)-1 {
  239. attachmentText += "\n"
  240. }
  241. }
  242. return &SlackPayload{
  243. Channel: slack.Channel,
  244. Text: text,
  245. Username: slack.Username,
  246. IconURL: slack.IconURL,
  247. Attachments: []SlackAttachment{{
  248. Color: slack.Color,
  249. Text: attachmentText,
  250. }},
  251. }, nil
  252. }
  253. func getSlackPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*SlackPayload, error) {
  254. senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
  255. titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index),
  256. fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title))
  257. var text, title, attachmentText string
  258. switch p.Action {
  259. case api.HookIssueOpened:
  260. text = fmt.Sprintf("[%s] Pull request submitted by %s", p.Repository.FullName, senderLink)
  261. title = titleLink
  262. attachmentText = SlackTextFormatter(p.PullRequest.Body)
  263. case api.HookIssueClosed:
  264. if p.PullRequest.HasMerged {
  265. text = fmt.Sprintf("[%s] Pull request merged: %s by %s", p.Repository.FullName, titleLink, senderLink)
  266. } else {
  267. text = fmt.Sprintf("[%s] Pull request closed: %s by %s", p.Repository.FullName, titleLink, senderLink)
  268. }
  269. case api.HookIssueReOpened:
  270. text = fmt.Sprintf("[%s] Pull request re-opened: %s by %s", p.Repository.FullName, titleLink, senderLink)
  271. case api.HookIssueEdited:
  272. text = fmt.Sprintf("[%s] Pull request edited: %s by %s", p.Repository.FullName, titleLink, senderLink)
  273. attachmentText = SlackTextFormatter(p.PullRequest.Body)
  274. case api.HookIssueAssigned:
  275. list := make([]string, len(p.PullRequest.Assignees))
  276. for i, user := range p.PullRequest.Assignees {
  277. list[i] = SlackLinkFormatter(setting.AppURL+user.UserName, user.UserName)
  278. }
  279. text = fmt.Sprintf("[%s] Pull request assigned to %s: %s by %s", p.Repository.FullName,
  280. strings.Join(list, ", "),
  281. titleLink, senderLink)
  282. case api.HookIssueUnassigned:
  283. text = fmt.Sprintf("[%s] Pull request unassigned: %s by %s", p.Repository.FullName, titleLink, senderLink)
  284. case api.HookIssueLabelUpdated:
  285. text = fmt.Sprintf("[%s] Pull request labels updated: %s by %s", p.Repository.FullName, titleLink, senderLink)
  286. case api.HookIssueLabelCleared:
  287. text = fmt.Sprintf("[%s] Pull request labels cleared: %s by %s", p.Repository.FullName, titleLink, senderLink)
  288. case api.HookIssueSynchronized:
  289. text = fmt.Sprintf("[%s] Pull request synchronized: %s by %s", p.Repository.FullName, titleLink, senderLink)
  290. case api.HookIssueMilestoned:
  291. text = fmt.Sprintf("[%s] Pull request milestoned: #%s %s", p.Repository.FullName, titleLink, senderLink)
  292. case api.HookIssueDemilestoned:
  293. text = fmt.Sprintf("[%s] Pull request milestone cleared: #%s %s", p.Repository.FullName, titleLink, senderLink)
  294. }
  295. return &SlackPayload{
  296. Channel: slack.Channel,
  297. Text: text,
  298. Username: slack.Username,
  299. IconURL: slack.IconURL,
  300. Attachments: []SlackAttachment{{
  301. Color: slack.Color,
  302. Title: title,
  303. Text: attachmentText,
  304. }},
  305. }, nil
  306. }
  307. func getSlackPullRequestApprovalPayload(p *api.PullRequestPayload, slack *SlackMeta, event HookEventType) (*SlackPayload, error) {
  308. senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
  309. titleLink := SlackLinkFormatter(fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index),
  310. fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title))
  311. var text, title, attachmentText string
  312. switch p.Action {
  313. case api.HookIssueSynchronized:
  314. action, err := parseHookPullRequestEventType(event)
  315. if err != nil {
  316. return nil, err
  317. }
  318. text = fmt.Sprintf("[%s] Pull request review %s : %s by %s", p.Repository.FullName, action, titleLink, senderLink)
  319. }
  320. return &SlackPayload{
  321. Channel: slack.Channel,
  322. Text: text,
  323. Username: slack.Username,
  324. IconURL: slack.IconURL,
  325. Attachments: []SlackAttachment{{
  326. Color: slack.Color,
  327. Title: title,
  328. Text: attachmentText,
  329. }},
  330. }, nil
  331. }
  332. func getSlackRepositoryPayload(p *api.RepositoryPayload, slack *SlackMeta) (*SlackPayload, error) {
  333. senderLink := SlackLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
  334. var text, title, attachmentText string
  335. switch p.Action {
  336. case api.HookRepoCreated:
  337. text = fmt.Sprintf("[%s] Repository created by %s", p.Repository.FullName, senderLink)
  338. title = p.Repository.HTMLURL
  339. case api.HookRepoDeleted:
  340. text = fmt.Sprintf("[%s] Repository deleted by %s", p.Repository.FullName, senderLink)
  341. }
  342. return &SlackPayload{
  343. Channel: slack.Channel,
  344. Text: text,
  345. Username: slack.Username,
  346. IconURL: slack.IconURL,
  347. Attachments: []SlackAttachment{{
  348. Color: slack.Color,
  349. Title: title,
  350. Text: attachmentText,
  351. }},
  352. }, nil
  353. }
  354. // GetSlackPayload converts a slack webhook into a SlackPayload
  355. func GetSlackPayload(p api.Payloader, event HookEventType, meta string) (*SlackPayload, error) {
  356. s := new(SlackPayload)
  357. slack := &SlackMeta{}
  358. if err := json.Unmarshal([]byte(meta), &slack); err != nil {
  359. return s, errors.New("GetSlackPayload meta json:" + err.Error())
  360. }
  361. switch event {
  362. case HookEventCreate:
  363. return getSlackCreatePayload(p.(*api.CreatePayload), slack)
  364. case HookEventDelete:
  365. return getSlackDeletePayload(p.(*api.DeletePayload), slack)
  366. case HookEventFork:
  367. return getSlackForkPayload(p.(*api.ForkPayload), slack)
  368. case HookEventIssues:
  369. return getSlackIssuesPayload(p.(*api.IssuePayload), slack)
  370. case HookEventIssueComment:
  371. return getSlackIssueCommentPayload(p.(*api.IssueCommentPayload), slack)
  372. case HookEventPush:
  373. return getSlackPushPayload(p.(*api.PushPayload), slack)
  374. case HookEventPullRequest:
  375. return getSlackPullRequestPayload(p.(*api.PullRequestPayload), slack)
  376. case HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment:
  377. return getSlackPullRequestApprovalPayload(p.(*api.PullRequestPayload), slack, event)
  378. case HookEventRepository:
  379. return getSlackRepositoryPayload(p.(*api.RepositoryPayload), slack)
  380. case HookEventRelease:
  381. return getSlackReleasePayload(p.(*api.ReleasePayload), slack)
  382. }
  383. return s, nil
  384. }