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.

webhook_discord.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. // Copyright 2017 The Gitea 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. "strconv"
  10. "strings"
  11. "code.gitea.io/gitea/modules/git"
  12. "code.gitea.io/gitea/modules/setting"
  13. api "code.gitea.io/gitea/modules/structs"
  14. )
  15. type (
  16. // DiscordEmbedFooter for Embed Footer Structure.
  17. DiscordEmbedFooter struct {
  18. Text string `json:"text"`
  19. }
  20. // DiscordEmbedAuthor for Embed Author Structure
  21. DiscordEmbedAuthor struct {
  22. Name string `json:"name"`
  23. URL string `json:"url"`
  24. IconURL string `json:"icon_url"`
  25. }
  26. // DiscordEmbedField for Embed Field Structure
  27. DiscordEmbedField struct {
  28. Name string `json:"name"`
  29. Value string `json:"value"`
  30. }
  31. // DiscordEmbed is for Embed Structure
  32. DiscordEmbed struct {
  33. Title string `json:"title"`
  34. Description string `json:"description"`
  35. URL string `json:"url"`
  36. Color int `json:"color"`
  37. Footer DiscordEmbedFooter `json:"footer"`
  38. Author DiscordEmbedAuthor `json:"author"`
  39. Fields []DiscordEmbedField `json:"fields"`
  40. }
  41. // DiscordPayload represents
  42. DiscordPayload struct {
  43. Wait bool `json:"wait"`
  44. Content string `json:"content"`
  45. Username string `json:"username"`
  46. AvatarURL string `json:"avatar_url"`
  47. TTS bool `json:"tts"`
  48. Embeds []DiscordEmbed `json:"embeds"`
  49. }
  50. // DiscordMeta contains the discord metadata
  51. DiscordMeta struct {
  52. Username string `json:"username"`
  53. IconURL string `json:"icon_url"`
  54. }
  55. )
  56. func color(clr string) int {
  57. if clr != "" {
  58. clr = strings.TrimLeft(clr, "#")
  59. if s, err := strconv.ParseInt(clr, 16, 32); err == nil {
  60. return int(s)
  61. }
  62. }
  63. return 0
  64. }
  65. var (
  66. successColor = color("1ac600")
  67. warnColor = color("ffd930")
  68. failedColor = color("ff3232")
  69. )
  70. // SetSecret sets the discord secret
  71. func (p *DiscordPayload) SetSecret(_ string) {}
  72. // JSONPayload Marshals the DiscordPayload to json
  73. func (p *DiscordPayload) JSONPayload() ([]byte, error) {
  74. data, err := json.MarshalIndent(p, "", " ")
  75. if err != nil {
  76. return []byte{}, err
  77. }
  78. return data, nil
  79. }
  80. func getDiscordCreatePayload(p *api.CreatePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  81. // created tag/branch
  82. refName := git.RefEndName(p.Ref)
  83. title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
  84. return &DiscordPayload{
  85. Username: meta.Username,
  86. AvatarURL: meta.IconURL,
  87. Embeds: []DiscordEmbed{
  88. {
  89. Title: title,
  90. URL: p.Repo.HTMLURL + "/src/" + refName,
  91. Color: successColor,
  92. Author: DiscordEmbedAuthor{
  93. Name: p.Sender.UserName,
  94. URL: setting.AppURL + p.Sender.UserName,
  95. IconURL: p.Sender.AvatarURL,
  96. },
  97. },
  98. },
  99. }, nil
  100. }
  101. func getDiscordDeletePayload(p *api.DeletePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  102. // deleted tag/branch
  103. refName := git.RefEndName(p.Ref)
  104. title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
  105. return &DiscordPayload{
  106. Username: meta.Username,
  107. AvatarURL: meta.IconURL,
  108. Embeds: []DiscordEmbed{
  109. {
  110. Title: title,
  111. URL: p.Repo.HTMLURL + "/src/" + refName,
  112. Color: warnColor,
  113. Author: DiscordEmbedAuthor{
  114. Name: p.Sender.UserName,
  115. URL: setting.AppURL + p.Sender.UserName,
  116. IconURL: p.Sender.AvatarURL,
  117. },
  118. },
  119. },
  120. }, nil
  121. }
  122. func getDiscordForkPayload(p *api.ForkPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  123. // fork
  124. title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
  125. return &DiscordPayload{
  126. Username: meta.Username,
  127. AvatarURL: meta.IconURL,
  128. Embeds: []DiscordEmbed{
  129. {
  130. Title: title,
  131. URL: p.Repo.HTMLURL,
  132. Color: successColor,
  133. Author: DiscordEmbedAuthor{
  134. Name: p.Sender.UserName,
  135. URL: setting.AppURL + p.Sender.UserName,
  136. IconURL: p.Sender.AvatarURL,
  137. },
  138. },
  139. },
  140. }, nil
  141. }
  142. func getDiscordPushPayload(p *api.PushPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  143. var (
  144. branchName = git.RefEndName(p.Ref)
  145. commitDesc string
  146. )
  147. var titleLink string
  148. if len(p.Commits) == 1 {
  149. commitDesc = "1 new commit"
  150. titleLink = p.Commits[0].URL
  151. } else {
  152. commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
  153. titleLink = p.CompareURL
  154. }
  155. if titleLink == "" {
  156. titleLink = p.Repo.HTMLURL + "/src/" + branchName
  157. }
  158. title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
  159. var text string
  160. // for each commit, generate attachment text
  161. for i, commit := range p.Commits {
  162. text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
  163. strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
  164. // add linebreak to each commit but the last
  165. if i < len(p.Commits)-1 {
  166. text += "\n"
  167. }
  168. }
  169. return &DiscordPayload{
  170. Username: meta.Username,
  171. AvatarURL: meta.IconURL,
  172. Embeds: []DiscordEmbed{
  173. {
  174. Title: title,
  175. Description: text,
  176. URL: titleLink,
  177. Color: successColor,
  178. Author: DiscordEmbedAuthor{
  179. Name: p.Sender.UserName,
  180. URL: setting.AppURL + p.Sender.UserName,
  181. IconURL: p.Sender.AvatarURL,
  182. },
  183. },
  184. },
  185. }, nil
  186. }
  187. func getDiscordIssuesPayload(p *api.IssuePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  188. var text, title string
  189. var color int
  190. url := fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
  191. switch p.Action {
  192. case api.HookIssueOpened:
  193. title = fmt.Sprintf("[%s] Issue opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  194. text = p.Issue.Body
  195. color = warnColor
  196. case api.HookIssueClosed:
  197. title = fmt.Sprintf("[%s] Issue closed: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  198. color = failedColor
  199. text = p.Issue.Body
  200. case api.HookIssueReOpened:
  201. title = fmt.Sprintf("[%s] Issue re-opened: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  202. text = p.Issue.Body
  203. color = warnColor
  204. case api.HookIssueEdited:
  205. title = fmt.Sprintf("[%s] Issue edited: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  206. text = p.Issue.Body
  207. color = warnColor
  208. case api.HookIssueAssigned:
  209. title = fmt.Sprintf("[%s] Issue assigned to %s: #%d %s", p.Repository.FullName,
  210. p.Issue.Assignee.UserName, p.Index, p.Issue.Title)
  211. text = p.Issue.Body
  212. color = successColor
  213. case api.HookIssueUnassigned:
  214. title = fmt.Sprintf("[%s] Issue unassigned: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  215. text = p.Issue.Body
  216. color = warnColor
  217. case api.HookIssueLabelUpdated:
  218. title = fmt.Sprintf("[%s] Issue labels updated: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  219. text = p.Issue.Body
  220. color = warnColor
  221. case api.HookIssueLabelCleared:
  222. title = fmt.Sprintf("[%s] Issue labels cleared: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  223. text = p.Issue.Body
  224. color = warnColor
  225. case api.HookIssueSynchronized:
  226. title = fmt.Sprintf("[%s] Issue synchronized: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  227. text = p.Issue.Body
  228. color = warnColor
  229. case api.HookIssueMilestoned:
  230. title = fmt.Sprintf("[%s] Issue milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  231. text = p.Issue.Body
  232. color = warnColor
  233. case api.HookIssueDemilestoned:
  234. title = fmt.Sprintf("[%s] Issue clear milestone: #%d %s", p.Repository.FullName, p.Index, p.Issue.Title)
  235. text = p.Issue.Body
  236. color = warnColor
  237. }
  238. return &DiscordPayload{
  239. Username: meta.Username,
  240. AvatarURL: meta.IconURL,
  241. Embeds: []DiscordEmbed{
  242. {
  243. Title: title,
  244. Description: text,
  245. URL: url,
  246. Color: color,
  247. Author: DiscordEmbedAuthor{
  248. Name: p.Sender.UserName,
  249. URL: setting.AppURL + p.Sender.UserName,
  250. IconURL: p.Sender.AvatarURL,
  251. },
  252. },
  253. },
  254. }, nil
  255. }
  256. func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordMeta) (*DiscordPayload, error) {
  257. title := fmt.Sprintf("#%d %s", p.Issue.Index, p.Issue.Title)
  258. url := fmt.Sprintf("%s/issues/%d#%s", p.Repository.HTMLURL, p.Issue.Index, CommentHashTag(p.Comment.ID))
  259. content := ""
  260. var color int
  261. switch p.Action {
  262. case api.HookIssueCommentCreated:
  263. title = "New comment: " + title
  264. content = p.Comment.Body
  265. color = successColor
  266. case api.HookIssueCommentEdited:
  267. title = "Comment edited: " + title
  268. content = p.Comment.Body
  269. color = warnColor
  270. case api.HookIssueCommentDeleted:
  271. title = "Comment deleted: " + title
  272. url = fmt.Sprintf("%s/issues/%d", p.Repository.HTMLURL, p.Issue.Index)
  273. content = p.Comment.Body
  274. color = warnColor
  275. }
  276. return &DiscordPayload{
  277. Username: discord.Username,
  278. AvatarURL: discord.IconURL,
  279. Embeds: []DiscordEmbed{
  280. {
  281. Title: title,
  282. Description: content,
  283. URL: url,
  284. Color: color,
  285. Author: DiscordEmbedAuthor{
  286. Name: p.Sender.UserName,
  287. URL: setting.AppURL + p.Sender.UserName,
  288. IconURL: p.Sender.AvatarURL,
  289. },
  290. },
  291. },
  292. }, nil
  293. }
  294. func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  295. var text, title string
  296. var color int
  297. switch p.Action {
  298. case api.HookIssueOpened:
  299. title = fmt.Sprintf("[%s] Pull request opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  300. text = p.PullRequest.Body
  301. color = warnColor
  302. case api.HookIssueClosed:
  303. if p.PullRequest.HasMerged {
  304. title = fmt.Sprintf("[%s] Pull request merged: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  305. color = successColor
  306. } else {
  307. title = fmt.Sprintf("[%s] Pull request closed: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  308. color = failedColor
  309. }
  310. text = p.PullRequest.Body
  311. case api.HookIssueReOpened:
  312. title = fmt.Sprintf("[%s] Pull request re-opened: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  313. text = p.PullRequest.Body
  314. color = warnColor
  315. case api.HookIssueEdited:
  316. title = fmt.Sprintf("[%s] Pull request edited: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  317. text = p.PullRequest.Body
  318. color = warnColor
  319. case api.HookIssueAssigned:
  320. list := make([]string, len(p.PullRequest.Assignees))
  321. for i, user := range p.PullRequest.Assignees {
  322. list[i] = user.UserName
  323. }
  324. title = fmt.Sprintf("[%s] Pull request assigned to %s: #%d by %s", p.Repository.FullName,
  325. strings.Join(list, ", "),
  326. p.Index, p.PullRequest.Title)
  327. text = p.PullRequest.Body
  328. color = successColor
  329. case api.HookIssueUnassigned:
  330. title = fmt.Sprintf("[%s] Pull request unassigned: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  331. text = p.PullRequest.Body
  332. color = warnColor
  333. case api.HookIssueLabelUpdated:
  334. title = fmt.Sprintf("[%s] Pull request labels updated: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  335. text = p.PullRequest.Body
  336. color = warnColor
  337. case api.HookIssueLabelCleared:
  338. title = fmt.Sprintf("[%s] Pull request labels cleared: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  339. text = p.PullRequest.Body
  340. color = warnColor
  341. case api.HookIssueSynchronized:
  342. title = fmt.Sprintf("[%s] Pull request synchronized: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  343. text = p.PullRequest.Body
  344. color = warnColor
  345. case api.HookIssueMilestoned:
  346. title = fmt.Sprintf("[%s] Pull request milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  347. text = p.PullRequest.Body
  348. color = warnColor
  349. case api.HookIssueDemilestoned:
  350. title = fmt.Sprintf("[%s] Pull request clear milestone: #%d %s", p.Repository.FullName, p.Index, p.PullRequest.Title)
  351. text = p.PullRequest.Body
  352. color = warnColor
  353. }
  354. return &DiscordPayload{
  355. Username: meta.Username,
  356. AvatarURL: meta.IconURL,
  357. Embeds: []DiscordEmbed{
  358. {
  359. Title: title,
  360. Description: text,
  361. URL: p.PullRequest.HTMLURL,
  362. Color: color,
  363. Author: DiscordEmbedAuthor{
  364. Name: p.Sender.UserName,
  365. URL: setting.AppURL + p.Sender.UserName,
  366. IconURL: p.Sender.AvatarURL,
  367. },
  368. },
  369. },
  370. }, nil
  371. }
  372. func getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *DiscordMeta, event HookEventType) (*DiscordPayload, error) {
  373. var text, title string
  374. var color int
  375. switch p.Action {
  376. case api.HookIssueSynchronized:
  377. action, err := parseHookPullRequestEventType(event)
  378. if err != nil {
  379. return nil, err
  380. }
  381. title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
  382. text = p.PullRequest.Body
  383. color = warnColor
  384. }
  385. return &DiscordPayload{
  386. Username: meta.Username,
  387. AvatarURL: meta.IconURL,
  388. Embeds: []DiscordEmbed{
  389. {
  390. Title: title,
  391. Description: text,
  392. URL: p.PullRequest.HTMLURL,
  393. Color: color,
  394. Author: DiscordEmbedAuthor{
  395. Name: p.Sender.UserName,
  396. URL: setting.AppURL + p.Sender.UserName,
  397. IconURL: p.Sender.AvatarURL,
  398. },
  399. },
  400. },
  401. }, nil
  402. }
  403. func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  404. var title, url string
  405. var color int
  406. switch p.Action {
  407. case api.HookRepoCreated:
  408. title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
  409. url = p.Repository.HTMLURL
  410. color = successColor
  411. case api.HookRepoDeleted:
  412. title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
  413. color = warnColor
  414. }
  415. return &DiscordPayload{
  416. Username: meta.Username,
  417. AvatarURL: meta.IconURL,
  418. Embeds: []DiscordEmbed{
  419. {
  420. Title: title,
  421. URL: url,
  422. Color: color,
  423. Author: DiscordEmbedAuthor{
  424. Name: p.Sender.UserName,
  425. URL: setting.AppURL + p.Sender.UserName,
  426. IconURL: p.Sender.AvatarURL,
  427. },
  428. },
  429. },
  430. }, nil
  431. }
  432. func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  433. var title, url string
  434. var color int
  435. switch p.Action {
  436. case api.HookReleasePublished:
  437. title = fmt.Sprintf("[%s] Release created", p.Release.TagName)
  438. url = p.Release.URL
  439. color = successColor
  440. case api.HookReleaseUpdated:
  441. title = fmt.Sprintf("[%s] Release updated", p.Release.TagName)
  442. url = p.Release.URL
  443. color = successColor
  444. case api.HookReleaseDeleted:
  445. title = fmt.Sprintf("[%s] Release deleted", p.Release.TagName)
  446. url = p.Release.URL
  447. color = successColor
  448. }
  449. return &DiscordPayload{
  450. Username: meta.Username,
  451. AvatarURL: meta.IconURL,
  452. Embeds: []DiscordEmbed{
  453. {
  454. Title: title,
  455. Description: p.Release.Note,
  456. URL: url,
  457. Color: color,
  458. Author: DiscordEmbedAuthor{
  459. Name: p.Sender.UserName,
  460. URL: setting.AppURL + p.Sender.UserName,
  461. IconURL: p.Sender.AvatarURL,
  462. },
  463. },
  464. },
  465. }, nil
  466. }
  467. // GetDiscordPayload converts a discord webhook into a DiscordPayload
  468. func GetDiscordPayload(p api.Payloader, event HookEventType, meta string) (*DiscordPayload, error) {
  469. s := new(DiscordPayload)
  470. discord := &DiscordMeta{}
  471. if err := json.Unmarshal([]byte(meta), &discord); err != nil {
  472. return s, errors.New("GetDiscordPayload meta json:" + err.Error())
  473. }
  474. switch event {
  475. case HookEventCreate:
  476. return getDiscordCreatePayload(p.(*api.CreatePayload), discord)
  477. case HookEventDelete:
  478. return getDiscordDeletePayload(p.(*api.DeletePayload), discord)
  479. case HookEventFork:
  480. return getDiscordForkPayload(p.(*api.ForkPayload), discord)
  481. case HookEventIssues:
  482. return getDiscordIssuesPayload(p.(*api.IssuePayload), discord)
  483. case HookEventIssueComment:
  484. return getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), discord)
  485. case HookEventPush:
  486. return getDiscordPushPayload(p.(*api.PushPayload), discord)
  487. case HookEventPullRequest:
  488. return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
  489. case HookEventPullRequestRejected, HookEventPullRequestApproved, HookEventPullRequestComment:
  490. return getDiscordPullRequestApprovalPayload(p.(*api.PullRequestPayload), discord, event)
  491. case HookEventRepository:
  492. return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord)
  493. case HookEventRelease:
  494. return getDiscordReleasePayload(p.(*api.ReleasePayload), discord)
  495. }
  496. return s, nil
  497. }
  498. func parseHookPullRequestEventType(event HookEventType) (string, error) {
  499. switch event {
  500. case HookEventPullRequestApproved:
  501. return "approved", nil
  502. case HookEventPullRequestRejected:
  503. return "rejected", nil
  504. case HookEventPullRequestComment:
  505. return "comment", nil
  506. default:
  507. return "", errors.New("unknown event type")
  508. }
  509. }