webhook_discord.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Copyright 2018 Gitote. All rights reserved.
  3. //
  4. // This source code is licensed under the MIT license found in the
  5. // LICENSE file in the root directory of this source tree.
  6. package models
  7. import (
  8. "fmt"
  9. "gitote/gitote/pkg/setting"
  10. "strconv"
  11. "strings"
  12. "github.com/json-iterator/go"
  13. "gitlab.com/gitote/git-module"
  14. api "gitlab.com/gitote/go-gitote-client"
  15. )
  16. type DiscordEmbedFooterObject struct {
  17. Text string `json:"text"`
  18. }
  19. type DiscordEmbedAuthorObject struct {
  20. Name string `json:"name"`
  21. URL string `json:"url"`
  22. IconURL string `json:"icon_url"`
  23. }
  24. type DiscordEmbedFieldObject struct {
  25. Name string `json:"name"`
  26. Value string `json:"value"`
  27. }
  28. type DiscordEmbedObject struct {
  29. Title string `json:"title"`
  30. Description string `json:"description"`
  31. URL string `json:"url"`
  32. Color int `json:"color"`
  33. Footer *DiscordEmbedFooterObject `json:"footer"`
  34. Author *DiscordEmbedAuthorObject `json:"author"`
  35. Fields []*DiscordEmbedFieldObject `json:"fields"`
  36. }
  37. type DiscordPayload struct {
  38. Content string `json:"content"`
  39. Username string `json:"username"`
  40. AvatarURL string `json:"avatar_url"`
  41. Embeds []*DiscordEmbedObject `json:"embeds"`
  42. }
  43. func (p *DiscordPayload) JSONPayload() ([]byte, error) {
  44. data, err := jsoniter.MarshalIndent(p, "", " ")
  45. if err != nil {
  46. return []byte{}, err
  47. }
  48. return data, nil
  49. }
  50. // DiscordTextFormatter formats text
  51. func DiscordTextFormatter(s string) string {
  52. return strings.Split(s, "\n")[0]
  53. }
  54. // DiscordLinkFormatter formats link
  55. func DiscordLinkFormatter(url string, text string) string {
  56. return fmt.Sprintf("[%s](%s)", text, url)
  57. }
  58. // DiscordSHALinkFormatter formats SHA text
  59. func DiscordSHALinkFormatter(url string, text string) string {
  60. return fmt.Sprintf("[`%s`](%s)", text, url)
  61. }
  62. // getDiscordCreatePayload composes Discord payload for create new branch or tag.
  63. func getDiscordCreatePayload(p *api.CreatePayload) (*DiscordPayload, error) {
  64. refName := git.RefEndName(p.Ref)
  65. repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  66. refLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+refName, refName)
  67. content := fmt.Sprintf("Created new %s: %s/%s", p.RefType, repoLink, refLink)
  68. return &DiscordPayload{
  69. Embeds: []*DiscordEmbedObject{{
  70. Description: content,
  71. URL: setting.AppURL + p.Sender.UserName,
  72. Author: &DiscordEmbedAuthorObject{
  73. Name: p.Sender.UserName,
  74. IconURL: p.Sender.AvatarUrl,
  75. },
  76. }},
  77. }, nil
  78. }
  79. // getDiscordDeletePayload composes Discord payload for delete a branch or tag.
  80. func getDiscordDeletePayload(p *api.DeletePayload) (*DiscordPayload, error) {
  81. refName := git.RefEndName(p.Ref)
  82. repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  83. content := fmt.Sprintf("Deleted %s: %s/%s", p.RefType, repoLink, refName)
  84. return &DiscordPayload{
  85. Embeds: []*DiscordEmbedObject{{
  86. Description: content,
  87. URL: setting.AppURL + p.Sender.UserName,
  88. Author: &DiscordEmbedAuthorObject{
  89. Name: p.Sender.UserName,
  90. IconURL: p.Sender.AvatarUrl,
  91. },
  92. }},
  93. }, nil
  94. }
  95. // getDiscordForkPayload composes Discord payload for forked by a repository.
  96. func getDiscordForkPayload(p *api.ForkPayload) (*DiscordPayload, error) {
  97. baseLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  98. forkLink := DiscordLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
  99. content := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
  100. return &DiscordPayload{
  101. Embeds: []*DiscordEmbedObject{{
  102. Description: content,
  103. URL: setting.AppURL + p.Sender.UserName,
  104. Author: &DiscordEmbedAuthorObject{
  105. Name: p.Sender.UserName,
  106. IconURL: p.Sender.AvatarUrl,
  107. },
  108. }},
  109. }, nil
  110. }
  111. func getDiscordPushPayload(p *api.PushPayload, slack *SlackMeta) (*DiscordPayload, error) {
  112. // n new commits
  113. var (
  114. branchName = git.RefEndName(p.Ref)
  115. commitDesc string
  116. commitString string
  117. )
  118. if len(p.Commits) == 1 {
  119. commitDesc = "1 new commit"
  120. } else {
  121. commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
  122. }
  123. if len(p.CompareURL) > 0 {
  124. commitString = DiscordLinkFormatter(p.CompareURL, commitDesc)
  125. } else {
  126. commitString = commitDesc
  127. }
  128. repoLink := DiscordLinkFormatter(p.Repo.HTMLURL, p.Repo.Name)
  129. branchLink := DiscordLinkFormatter(p.Repo.HTMLURL+"/src/"+branchName, branchName)
  130. content := fmt.Sprintf("Pushed %s to %s/%s\n", commitString, repoLink, branchLink)
  131. // for each commit, generate attachment text
  132. for i, commit := range p.Commits {
  133. content += fmt.Sprintf("%s %s - %s", DiscordSHALinkFormatter(commit.URL, commit.ID[:7]), DiscordTextFormatter(commit.Message), commit.Author.Name)
  134. // add linebreak to each commit but the last
  135. if i < len(p.Commits)-1 {
  136. content += "\n"
  137. }
  138. }
  139. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  140. return &DiscordPayload{
  141. Username: slack.Username,
  142. AvatarURL: slack.IconURL,
  143. Embeds: []*DiscordEmbedObject{{
  144. Description: content,
  145. URL: setting.AppURL + p.Sender.UserName,
  146. Color: int(color),
  147. Author: &DiscordEmbedAuthorObject{
  148. Name: p.Sender.UserName,
  149. IconURL: p.Sender.AvatarUrl,
  150. },
  151. }},
  152. }, nil
  153. }
  154. func getDiscordIssuesPayload(p *api.IssuesPayload, slack *SlackMeta) (*DiscordPayload, error) {
  155. title := fmt.Sprintf("#%d %s", p.Index, p.Issue.Title)
  156. url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Index)
  157. content := ""
  158. fields := make([]*DiscordEmbedFieldObject, 0, 1)
  159. switch p.Action {
  160. case api.HOOK_ISSUE_OPENED:
  161. title = "New issue: " + title
  162. content = p.Issue.Body
  163. case api.HOOK_ISSUE_CLOSED:
  164. title = "Issue closed: " + title
  165. case api.HOOK_ISSUE_REOPENED:
  166. title = "Issue re-opened: " + title
  167. case api.HOOK_ISSUE_EDITED:
  168. title = "Issue edited: " + title
  169. content = p.Issue.Body
  170. case api.HOOK_ISSUE_ASSIGNED:
  171. title = "Issue assigned: " + title
  172. fields = []*DiscordEmbedFieldObject{{
  173. Name: "New Assignee",
  174. Value: p.Issue.Assignee.UserName,
  175. }}
  176. case api.HOOK_ISSUE_UNASSIGNED:
  177. title = "Issue unassigned: " + title
  178. case api.HOOK_ISSUE_LABEL_UPDATED:
  179. title = "Issue labels updated: " + title
  180. labels := make([]string, len(p.Issue.Labels))
  181. for i := range p.Issue.Labels {
  182. labels[i] = p.Issue.Labels[i].Name
  183. }
  184. if len(labels) == 0 {
  185. labels = []string{"<empty>"}
  186. }
  187. fields = []*DiscordEmbedFieldObject{{
  188. Name: "Labels",
  189. Value: strings.Join(labels, ", "),
  190. }}
  191. case api.HOOK_ISSUE_LABEL_CLEARED:
  192. title = "Issue labels cleared: " + title
  193. case api.HOOK_ISSUE_SYNCHRONIZED:
  194. title = "Issue synchronized: " + title
  195. case api.HOOK_ISSUE_MILESTONED:
  196. title = "Issue milestoned: " + title
  197. fields = []*DiscordEmbedFieldObject{{
  198. Name: "New Milestone",
  199. Value: p.Issue.Milestone.Title,
  200. }}
  201. case api.HOOK_ISSUE_DEMILESTONED:
  202. title = "Issue demilestoned: " + title
  203. }
  204. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  205. return &DiscordPayload{
  206. Username: slack.Username,
  207. AvatarURL: slack.IconURL,
  208. Embeds: []*DiscordEmbedObject{{
  209. Title: title,
  210. Description: content,
  211. URL: url,
  212. Color: int(color),
  213. Footer: &DiscordEmbedFooterObject{
  214. Text: p.Repository.FullName,
  215. },
  216. Author: &DiscordEmbedAuthorObject{
  217. Name: p.Sender.UserName,
  218. IconURL: p.Sender.AvatarUrl,
  219. },
  220. Fields: fields,
  221. }},
  222. }, nil
  223. }
  224. func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, slack *SlackMeta) (*DiscordPayload, error) {
  225. title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
  226. url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID))
  227. content := ""
  228. fields := make([]*DiscordEmbedFieldObject, 0, 1)
  229. switch p.Action {
  230. case api.HOOK_ISSUE_COMMENT_CREATED:
  231. title = "New comment: " + title
  232. content = p.Comment.Body
  233. case api.HOOK_ISSUE_COMMENT_EDITED:
  234. title = "Comment edited: " + title
  235. content = p.Comment.Body
  236. case api.HOOK_ISSUE_COMMENT_DELETED:
  237. title = "Comment deleted: " + title
  238. url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
  239. content = p.Comment.Body
  240. }
  241. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  242. return &DiscordPayload{
  243. Username: slack.Username,
  244. AvatarURL: slack.IconURL,
  245. Embeds: []*DiscordEmbedObject{{
  246. Title: title,
  247. Description: content,
  248. URL: url,
  249. Color: int(color),
  250. Footer: &DiscordEmbedFooterObject{
  251. Text: p.Repository.FullName,
  252. },
  253. Author: &DiscordEmbedAuthorObject{
  254. Name: p.Sender.UserName,
  255. IconURL: p.Sender.AvatarUrl,
  256. },
  257. Fields: fields,
  258. }},
  259. }, nil
  260. }
  261. func getDiscordPullRequestPayload(p *api.PullRequestPayload, slack *SlackMeta) (*DiscordPayload, error) {
  262. title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
  263. url := fmt.Sprintf("%s/pulls/%d", p.Repository.HTMLURL, p.Index)
  264. content := ""
  265. fields := make([]*DiscordEmbedFieldObject, 0, 1)
  266. switch p.Action {
  267. case api.HOOK_ISSUE_OPENED:
  268. title = "New pull request: " + title
  269. content = p.PullRequest.Body
  270. case api.HOOK_ISSUE_CLOSED:
  271. if p.PullRequest.HasMerged {
  272. title = "Pull request merged: " + title
  273. } else {
  274. title = "Pull request closed: " + title
  275. }
  276. case api.HOOK_ISSUE_REOPENED:
  277. title = "Pull request re-opened: " + title
  278. case api.HOOK_ISSUE_EDITED:
  279. title = "Pull request edited: " + title
  280. content = p.PullRequest.Body
  281. case api.HOOK_ISSUE_ASSIGNED:
  282. title = "Pull request assigned: " + title
  283. fields = []*DiscordEmbedFieldObject{{
  284. Name: "New Assignee",
  285. Value: p.PullRequest.Assignee.UserName,
  286. }}
  287. case api.HOOK_ISSUE_UNASSIGNED:
  288. title = "Pull request unassigned: " + title
  289. case api.HOOK_ISSUE_LABEL_UPDATED:
  290. title = "Pull request labels updated: " + title
  291. labels := make([]string, len(p.PullRequest.Labels))
  292. for i := range p.PullRequest.Labels {
  293. labels[i] = p.PullRequest.Labels[i].Name
  294. }
  295. fields = []*DiscordEmbedFieldObject{{
  296. Name: "Labels",
  297. Value: strings.Join(labels, ", "),
  298. }}
  299. case api.HOOK_ISSUE_LABEL_CLEARED:
  300. title = "Pull request labels cleared: " + title
  301. case api.HOOK_ISSUE_SYNCHRONIZED:
  302. title = "Pull request synchronized: " + title
  303. case api.HOOK_ISSUE_MILESTONED:
  304. title = "Pull request milestoned: " + title
  305. fields = []*DiscordEmbedFieldObject{{
  306. Name: "New Milestone",
  307. Value: p.PullRequest.Milestone.Title,
  308. }}
  309. case api.HOOK_ISSUE_DEMILESTONED:
  310. title = "Pull request demilestoned: " + title
  311. }
  312. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  313. return &DiscordPayload{
  314. Username: slack.Username,
  315. AvatarURL: slack.IconURL,
  316. Embeds: []*DiscordEmbedObject{{
  317. Title: title,
  318. Description: content,
  319. URL: url,
  320. Color: int(color),
  321. Footer: &DiscordEmbedFooterObject{
  322. Text: p.Repository.FullName,
  323. },
  324. Author: &DiscordEmbedAuthorObject{
  325. Name: p.Sender.UserName,
  326. IconURL: p.Sender.AvatarUrl,
  327. },
  328. Fields: fields,
  329. }},
  330. }, nil
  331. }
  332. func getDiscordReleasePayload(p *api.ReleasePayload) (*DiscordPayload, error) {
  333. repoLink := DiscordLinkFormatter(p.Repository.HTMLURL, p.Repository.Name)
  334. refLink := DiscordLinkFormatter(p.Repository.HTMLURL+"/src/"+p.Release.TagName, p.Release.TagName)
  335. content := fmt.Sprintf("Published new release %s of %s", refLink, repoLink)
  336. return &DiscordPayload{
  337. Embeds: []*DiscordEmbedObject{{
  338. Description: content,
  339. URL: setting.AppURL + p.Sender.UserName,
  340. Author: &DiscordEmbedAuthorObject{
  341. Name: p.Sender.UserName,
  342. IconURL: p.Sender.AvatarUrl,
  343. },
  344. }},
  345. }, nil
  346. }
  347. // GetDiscordPayload converts a discord webhook into a DiscordPayload
  348. func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (payload *DiscordPayload, err error) {
  349. slack := &SlackMeta{}
  350. if err := jsoniter.Unmarshal([]byte(meta), &slack); err != nil {
  351. return nil, fmt.Errorf("jsoniter.Unmarshal: %v", err)
  352. }
  353. switch event {
  354. case HookEventCreate:
  355. payload, err = getDiscordCreatePayload(p.(*api.CreatePayload))
  356. case HookEventDelete:
  357. payload, err = getDiscordDeletePayload(p.(*api.DeletePayload))
  358. case HookEventFork:
  359. payload, err = getDiscordForkPayload(p.(*api.ForkPayload))
  360. case HookEventPush:
  361. payload, err = getDiscordPushPayload(p.(*api.PushPayload), slack)
  362. case HookEventIssues:
  363. payload, err = getDiscordIssuesPayload(p.(*api.IssuesPayload), slack)
  364. case HookEventIssueComment:
  365. payload, err = getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), slack)
  366. case HookEventPullRequest:
  367. payload, err = getDiscordPullRequestPayload(p.(*api.PullRequestPayload), slack)
  368. case HookEventRelease:
  369. payload, err = getDiscordReleasePayload(p.(*api.ReleasePayload))
  370. }
  371. if err != nil {
  372. return nil, fmt.Errorf("event '%s': %v", event, err)
  373. }
  374. payload.Username = slack.Username
  375. payload.AvatarURL = slack.IconURL
  376. if len(payload.Embeds) > 0 {
  377. color, _ := strconv.ParseInt(strings.TrimLeft(slack.Color, "#"), 16, 32)
  378. payload.Embeds[0].Color = int(color)
  379. }
  380. return payload, nil
  381. }