webhook_discord.go 13 KB

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