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

discord.go 17 kB

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