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.

432 lines
11 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 (d *DiscordPayload) SetSecret(_ string) {}
  87. // JSONPayload Marshals the DiscordPayload to json
  88. func (d *DiscordPayload) JSONPayload() ([]byte, error) {
  89. data, err := json.MarshalIndent(d, "", " ")
  90. if err != nil {
  91. return []byte{}, err
  92. }
  93. return data, nil
  94. }
  95. var (
  96. _ PayloadConvertor = &DiscordPayload{}
  97. )
  98. // Create implements PayloadConvertor Create method
  99. func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
  100. // created tag/branch
  101. refName := git.RefEndName(p.Ref)
  102. title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
  103. return &DiscordPayload{
  104. Username: d.Username,
  105. AvatarURL: d.AvatarURL,
  106. Embeds: []DiscordEmbed{
  107. {
  108. Title: title,
  109. URL: p.Repo.HTMLURL + "/src/" + refName,
  110. Color: greenColor,
  111. Author: DiscordEmbedAuthor{
  112. Name: p.Sender.UserName,
  113. URL: setting.AppURL + p.Sender.UserName,
  114. IconURL: p.Sender.AvatarURL,
  115. },
  116. },
  117. },
  118. }, nil
  119. }
  120. // Delete implements PayloadConvertor Delete method
  121. func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
  122. // deleted tag/branch
  123. refName := git.RefEndName(p.Ref)
  124. title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
  125. return &DiscordPayload{
  126. Username: d.Username,
  127. AvatarURL: d.AvatarURL,
  128. Embeds: []DiscordEmbed{
  129. {
  130. Title: title,
  131. URL: p.Repo.HTMLURL + "/src/" + refName,
  132. Color: redColor,
  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. // Fork implements PayloadConvertor Fork method
  143. func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
  144. title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
  145. return &DiscordPayload{
  146. Username: d.Username,
  147. AvatarURL: d.AvatarURL,
  148. Embeds: []DiscordEmbed{
  149. {
  150. Title: title,
  151. URL: p.Repo.HTMLURL,
  152. Color: greenColor,
  153. Author: DiscordEmbedAuthor{
  154. Name: p.Sender.UserName,
  155. URL: setting.AppURL + p.Sender.UserName,
  156. IconURL: p.Sender.AvatarURL,
  157. },
  158. },
  159. },
  160. }, nil
  161. }
  162. // Push implements PayloadConvertor Push method
  163. func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
  164. var (
  165. branchName = git.RefEndName(p.Ref)
  166. commitDesc string
  167. )
  168. var titleLink string
  169. if len(p.Commits) == 1 {
  170. commitDesc = "1 new commit"
  171. titleLink = p.Commits[0].URL
  172. } else {
  173. commitDesc = fmt.Sprintf("%d new commits", len(p.Commits))
  174. titleLink = p.CompareURL
  175. }
  176. if titleLink == "" {
  177. titleLink = p.Repo.HTMLURL + "/src/" + branchName
  178. }
  179. title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
  180. var text string
  181. // for each commit, generate attachment text
  182. for i, commit := range p.Commits {
  183. text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
  184. strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
  185. // add linebreak to each commit but the last
  186. if i < len(p.Commits)-1 {
  187. text += "\n"
  188. }
  189. }
  190. return &DiscordPayload{
  191. Username: d.Username,
  192. AvatarURL: d.AvatarURL,
  193. Embeds: []DiscordEmbed{
  194. {
  195. Title: title,
  196. Description: text,
  197. URL: titleLink,
  198. Color: greenColor,
  199. Author: DiscordEmbedAuthor{
  200. Name: p.Sender.UserName,
  201. URL: setting.AppURL + p.Sender.UserName,
  202. IconURL: p.Sender.AvatarURL,
  203. },
  204. },
  205. },
  206. }, nil
  207. }
  208. // Issue implements PayloadConvertor Issue method
  209. func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
  210. text, _, attachmentText, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
  211. return &DiscordPayload{
  212. Username: d.Username,
  213. AvatarURL: d.AvatarURL,
  214. Embeds: []DiscordEmbed{
  215. {
  216. Title: text,
  217. Description: attachmentText,
  218. URL: p.Issue.HTMLURL,
  219. Color: color,
  220. Author: DiscordEmbedAuthor{
  221. Name: p.Sender.UserName,
  222. URL: setting.AppURL + p.Sender.UserName,
  223. IconURL: p.Sender.AvatarURL,
  224. },
  225. },
  226. },
  227. }, nil
  228. }
  229. // IssueComment implements PayloadConvertor IssueComment method
  230. func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
  231. text, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
  232. return &DiscordPayload{
  233. Username: d.Username,
  234. AvatarURL: d.AvatarURL,
  235. Embeds: []DiscordEmbed{
  236. {
  237. Title: text,
  238. Description: p.Comment.Body,
  239. URL: p.Comment.HTMLURL,
  240. Color: color,
  241. Author: DiscordEmbedAuthor{
  242. Name: p.Sender.UserName,
  243. URL: setting.AppURL + p.Sender.UserName,
  244. IconURL: p.Sender.AvatarURL,
  245. },
  246. },
  247. },
  248. }, nil
  249. }
  250. // PullRequest implements PayloadConvertor PullRequest method
  251. func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
  252. text, _, attachmentText, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
  253. return &DiscordPayload{
  254. Username: d.Username,
  255. AvatarURL: d.AvatarURL,
  256. Embeds: []DiscordEmbed{
  257. {
  258. Title: text,
  259. Description: attachmentText,
  260. URL: p.PullRequest.HTMLURL,
  261. Color: color,
  262. Author: DiscordEmbedAuthor{
  263. Name: p.Sender.UserName,
  264. URL: setting.AppURL + p.Sender.UserName,
  265. IconURL: p.Sender.AvatarURL,
  266. },
  267. },
  268. },
  269. }, nil
  270. }
  271. // Review implements PayloadConvertor Review method
  272. func (d *DiscordPayload) Review(p *api.PullRequestPayload, event models.HookEventType) (api.Payloader, error) {
  273. var text, title string
  274. var color int
  275. switch p.Action {
  276. case api.HookIssueReviewed:
  277. action, err := parseHookPullRequestEventType(event)
  278. if err != nil {
  279. return nil, err
  280. }
  281. title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
  282. text = p.Review.Content
  283. switch event {
  284. case models.HookEventPullRequestReviewApproved:
  285. color = greenColor
  286. case models.HookEventPullRequestReviewRejected:
  287. color = redColor
  288. case models.HookEventPullRequestComment:
  289. color = greyColor
  290. default:
  291. color = yellowColor
  292. }
  293. }
  294. return &DiscordPayload{
  295. Username: d.Username,
  296. AvatarURL: d.AvatarURL,
  297. Embeds: []DiscordEmbed{
  298. {
  299. Title: title,
  300. Description: text,
  301. URL: p.PullRequest.HTMLURL,
  302. Color: color,
  303. Author: DiscordEmbedAuthor{
  304. Name: p.Sender.UserName,
  305. URL: setting.AppURL + p.Sender.UserName,
  306. IconURL: p.Sender.AvatarURL,
  307. },
  308. },
  309. },
  310. }, nil
  311. }
  312. // Repository implements PayloadConvertor Repository method
  313. func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
  314. var title, url string
  315. var color int
  316. switch p.Action {
  317. case api.HookRepoCreated:
  318. title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
  319. url = p.Repository.HTMLURL
  320. color = greenColor
  321. case api.HookRepoDeleted:
  322. title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
  323. color = redColor
  324. }
  325. return &DiscordPayload{
  326. Username: d.Username,
  327. AvatarURL: d.AvatarURL,
  328. Embeds: []DiscordEmbed{
  329. {
  330. Title: title,
  331. URL: url,
  332. Color: color,
  333. Author: DiscordEmbedAuthor{
  334. Name: p.Sender.UserName,
  335. URL: setting.AppURL + p.Sender.UserName,
  336. IconURL: p.Sender.AvatarURL,
  337. },
  338. },
  339. },
  340. }, nil
  341. }
  342. // Release implements PayloadConvertor Release method
  343. func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
  344. text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
  345. return &DiscordPayload{
  346. Username: d.Username,
  347. AvatarURL: d.AvatarURL,
  348. Embeds: []DiscordEmbed{
  349. {
  350. Title: text,
  351. Description: p.Release.Note,
  352. URL: p.Release.URL,
  353. Color: color,
  354. Author: DiscordEmbedAuthor{
  355. Name: p.Sender.UserName,
  356. URL: setting.AppURL + p.Sender.UserName,
  357. IconURL: p.Sender.AvatarURL,
  358. },
  359. },
  360. },
  361. }, nil
  362. }
  363. // GetDiscordPayload converts a discord webhook into a DiscordPayload
  364. func GetDiscordPayload(p api.Payloader, event models.HookEventType, meta string) (api.Payloader, error) {
  365. s := new(DiscordPayload)
  366. discord := &DiscordMeta{}
  367. if err := json.Unmarshal([]byte(meta), &discord); err != nil {
  368. return s, errors.New("GetDiscordPayload meta json:" + err.Error())
  369. }
  370. s.Username = discord.Username
  371. s.AvatarURL = discord.IconURL
  372. return convertPayloader(s, p, event)
  373. }
  374. func parseHookPullRequestEventType(event models.HookEventType) (string, error) {
  375. switch event {
  376. case models.HookEventPullRequestReviewApproved:
  377. return "approved", nil
  378. case models.HookEventPullRequestReviewRejected:
  379. return "rejected", nil
  380. case models.HookEventPullRequestComment:
  381. return "comment", nil
  382. default:
  383. return "", errors.New("unknown event type")
  384. }
  385. }