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.

442 lines
12 KiB

  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. text, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
  204. return &DiscordPayload{
  205. Username: meta.Username,
  206. AvatarURL: meta.IconURL,
  207. Embeds: []DiscordEmbed{
  208. {
  209. Title: text,
  210. Description: attachmentText,
  211. URL: p.Issue.HTMLURL,
  212. Color: color,
  213. Author: DiscordEmbedAuthor{
  214. Name: p.Sender.UserName,
  215. URL: setting.AppURL + p.Sender.UserName,
  216. IconURL: p.Sender.AvatarURL,
  217. },
  218. },
  219. },
  220. }, nil
  221. }
  222. func getDiscordIssueCommentPayload(p *api.IssueCommentPayload, discord *DiscordMeta) (*DiscordPayload, error) {
  223. text, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
  224. return &DiscordPayload{
  225. Username: discord.Username,
  226. AvatarURL: discord.IconURL,
  227. Embeds: []DiscordEmbed{
  228. {
  229. Title: text,
  230. Description: p.Comment.Body,
  231. URL: p.Comment.HTMLURL,
  232. Color: color,
  233. Author: DiscordEmbedAuthor{
  234. Name: p.Sender.UserName,
  235. URL: setting.AppURL + p.Sender.UserName,
  236. IconURL: p.Sender.AvatarURL,
  237. },
  238. },
  239. },
  240. }, nil
  241. }
  242. func getDiscordPullRequestPayload(p *api.PullRequestPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  243. text, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
  244. return &DiscordPayload{
  245. Username: meta.Username,
  246. AvatarURL: meta.IconURL,
  247. Embeds: []DiscordEmbed{
  248. {
  249. Title: text,
  250. Description: attachmentText,
  251. URL: p.PullRequest.HTMLURL,
  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 getDiscordPullRequestApprovalPayload(p *api.PullRequestPayload, meta *DiscordMeta, event models.HookEventType) (*DiscordPayload, error) {
  263. var text, title string
  264. var color int
  265. switch p.Action {
  266. case api.HookIssueReviewed:
  267. action, err := parseHookPullRequestEventType(event)
  268. if err != nil {
  269. return nil, err
  270. }
  271. title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
  272. text = p.Review.Content
  273. switch event {
  274. case models.HookEventPullRequestReviewApproved:
  275. color = greenColor
  276. case models.HookEventPullRequestReviewRejected:
  277. color = redColor
  278. case models.HookEventPullRequestComment:
  279. color = greyColor
  280. default:
  281. color = yellowColor
  282. }
  283. }
  284. return &DiscordPayload{
  285. Username: meta.Username,
  286. AvatarURL: meta.IconURL,
  287. Embeds: []DiscordEmbed{
  288. {
  289. Title: title,
  290. Description: text,
  291. URL: p.PullRequest.HTMLURL,
  292. Color: color,
  293. Author: DiscordEmbedAuthor{
  294. Name: p.Sender.UserName,
  295. URL: setting.AppURL + p.Sender.UserName,
  296. IconURL: p.Sender.AvatarURL,
  297. },
  298. },
  299. },
  300. }, nil
  301. }
  302. func getDiscordRepositoryPayload(p *api.RepositoryPayload, meta *DiscordMeta) (*DiscordPayload, error) {
  303. var title, url string
  304. var color int
  305. switch p.Action {
  306. case api.HookRepoCreated:
  307. title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
  308. url = p.Repository.HTMLURL
  309. color = greenColor
  310. case api.HookRepoDeleted:
  311. title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
  312. color = redColor
  313. }
  314. return &DiscordPayload{
  315. Username: meta.Username,
  316. AvatarURL: meta.IconURL,
  317. Embeds: []DiscordEmbed{
  318. {
  319. Title: title,
  320. URL: url,
  321. Color: color,
  322. Author: DiscordEmbedAuthor{
  323. Name: p.Sender.UserName,
  324. URL: setting.AppURL + p.Sender.UserName,
  325. IconURL: p.Sender.AvatarURL,
  326. },
  327. },
  328. },
  329. }, nil
  330. }
  331. func getDiscordReleasePayload(p *api.ReleasePayload, meta *DiscordMeta) (*DiscordPayload, error) {
  332. text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
  333. return &DiscordPayload{
  334. Username: meta.Username,
  335. AvatarURL: meta.IconURL,
  336. Embeds: []DiscordEmbed{
  337. {
  338. Title: text,
  339. Description: p.Release.Note,
  340. URL: p.Release.URL,
  341. Color: color,
  342. Author: DiscordEmbedAuthor{
  343. Name: p.Sender.UserName,
  344. URL: setting.AppURL + p.Sender.UserName,
  345. IconURL: p.Sender.AvatarURL,
  346. },
  347. },
  348. },
  349. }, nil
  350. }
  351. // GetDiscordPayload converts a discord webhook into a DiscordPayload
  352. func GetDiscordPayload(p api.Payloader, event models.HookEventType, meta string) (*DiscordPayload, error) {
  353. s := new(DiscordPayload)
  354. discord := &DiscordMeta{}
  355. if err := json.Unmarshal([]byte(meta), &discord); err != nil {
  356. return s, errors.New("GetDiscordPayload meta json:" + err.Error())
  357. }
  358. switch event {
  359. case models.HookEventCreate:
  360. return getDiscordCreatePayload(p.(*api.CreatePayload), discord)
  361. case models.HookEventDelete:
  362. return getDiscordDeletePayload(p.(*api.DeletePayload), discord)
  363. case models.HookEventFork:
  364. return getDiscordForkPayload(p.(*api.ForkPayload), discord)
  365. case models.HookEventIssues, models.HookEventIssueAssign, models.HookEventIssueLabel, models.HookEventIssueMilestone:
  366. return getDiscordIssuesPayload(p.(*api.IssuePayload), discord)
  367. case models.HookEventIssueComment, models.HookEventPullRequestComment:
  368. return getDiscordIssueCommentPayload(p.(*api.IssueCommentPayload), discord)
  369. case models.HookEventPush:
  370. return getDiscordPushPayload(p.(*api.PushPayload), discord)
  371. case models.HookEventPullRequest, models.HookEventPullRequestAssign, models.HookEventPullRequestLabel,
  372. models.HookEventPullRequestMilestone, models.HookEventPullRequestSync:
  373. return getDiscordPullRequestPayload(p.(*api.PullRequestPayload), discord)
  374. case models.HookEventPullRequestReviewRejected, models.HookEventPullRequestReviewApproved, models.HookEventPullRequestReviewComment:
  375. return getDiscordPullRequestApprovalPayload(p.(*api.PullRequestPayload), discord, event)
  376. case models.HookEventRepository:
  377. return getDiscordRepositoryPayload(p.(*api.RepositoryPayload), discord)
  378. case models.HookEventRelease:
  379. return getDiscordReleasePayload(p.(*api.ReleasePayload), discord)
  380. }
  381. return s, nil
  382. }
  383. func parseHookPullRequestEventType(event models.HookEventType) (string, error) {
  384. switch event {
  385. case models.HookEventPullRequestReviewApproved:
  386. return "approved", nil
  387. case models.HookEventPullRequestReviewRejected:
  388. return "rejected", nil
  389. case models.HookEventPullRequestComment:
  390. return "comment", nil
  391. default:
  392. return "", errors.New("unknown event type")
  393. }
  394. }