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.

discord.go 9.3KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package webhook
  4. import (
  5. "errors"
  6. "fmt"
  7. "net/url"
  8. "strconv"
  9. "strings"
  10. webhook_model "code.gitea.io/gitea/models/webhook"
  11. "code.gitea.io/gitea/modules/git"
  12. "code.gitea.io/gitea/modules/json"
  13. "code.gitea.io/gitea/modules/log"
  14. "code.gitea.io/gitea/modules/setting"
  15. api "code.gitea.io/gitea/modules/structs"
  16. "code.gitea.io/gitea/modules/util"
  17. webhook_module "code.gitea.io/gitea/modules/webhook"
  18. )
  19. type (
  20. // DiscordEmbedFooter for Embed Footer Structure.
  21. DiscordEmbedFooter struct {
  22. Text string `json:"text"`
  23. }
  24. // DiscordEmbedAuthor for Embed Author Structure
  25. DiscordEmbedAuthor struct {
  26. Name string `json:"name"`
  27. URL string `json:"url"`
  28. IconURL string `json:"icon_url"`
  29. }
  30. // DiscordEmbedField for Embed Field Structure
  31. DiscordEmbedField struct {
  32. Name string `json:"name"`
  33. Value string `json:"value"`
  34. }
  35. // DiscordEmbed is for Embed Structure
  36. DiscordEmbed struct {
  37. Title string `json:"title"`
  38. Description string `json:"description"`
  39. URL string `json:"url"`
  40. Color int `json:"color"`
  41. Footer DiscordEmbedFooter `json:"footer"`
  42. Author DiscordEmbedAuthor `json:"author"`
  43. Fields []DiscordEmbedField `json:"fields"`
  44. }
  45. // DiscordPayload represents
  46. DiscordPayload struct {
  47. Wait bool `json:"wait"`
  48. Content string `json:"content"`
  49. Username string `json:"username"`
  50. AvatarURL string `json:"avatar_url,omitempty"`
  51. TTS bool `json:"tts"`
  52. Embeds []DiscordEmbed `json:"embeds"`
  53. }
  54. // DiscordMeta contains the discord metadata
  55. DiscordMeta struct {
  56. Username string `json:"username"`
  57. IconURL string `json:"icon_url"`
  58. }
  59. )
  60. // GetDiscordHook returns discord metadata
  61. func GetDiscordHook(w *webhook_model.Webhook) *DiscordMeta {
  62. s := &DiscordMeta{}
  63. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  64. log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err)
  65. }
  66. return s
  67. }
  68. func color(clr string) int {
  69. if clr != "" {
  70. clr = strings.TrimLeft(clr, "#")
  71. if s, err := strconv.ParseInt(clr, 16, 32); err == nil {
  72. return int(s)
  73. }
  74. }
  75. return 0
  76. }
  77. var (
  78. greenColor = color("1ac600")
  79. greenColorLight = color("bfe5bf")
  80. yellowColor = color("ffd930")
  81. greyColor = color("4f545c")
  82. purpleColor = color("7289da")
  83. orangeColor = color("eb6420")
  84. orangeColorLight = color("e68d60")
  85. redColor = color("ff3232")
  86. )
  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 _ PayloadConvertor = &DiscordPayload{}
  96. // Create implements PayloadConvertor Create method
  97. func (d *DiscordPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
  98. // created tag/branch
  99. refName := git.RefName(p.Ref).ShortName()
  100. title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
  101. return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), greenColor), nil
  102. }
  103. // Delete implements PayloadConvertor Delete method
  104. func (d *DiscordPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
  105. // deleted tag/branch
  106. refName := git.RefName(p.Ref).ShortName()
  107. title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
  108. return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), redColor), nil
  109. }
  110. // Fork implements PayloadConvertor Fork method
  111. func (d *DiscordPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
  112. title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
  113. return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL, greenColor), nil
  114. }
  115. // Push implements PayloadConvertor Push method
  116. func (d *DiscordPayload) Push(p *api.PushPayload) (api.Payloader, error) {
  117. var (
  118. branchName = git.RefName(p.Ref).ShortName()
  119. commitDesc string
  120. )
  121. var titleLink string
  122. if p.TotalCommits == 1 {
  123. commitDesc = "1 new commit"
  124. titleLink = p.Commits[0].URL
  125. } else {
  126. commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
  127. titleLink = p.CompareURL
  128. }
  129. if titleLink == "" {
  130. titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName)
  131. }
  132. title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
  133. var text string
  134. // for each commit, generate attachment text
  135. for i, commit := range p.Commits {
  136. text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL,
  137. strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
  138. // add linebreak to each commit but the last
  139. if i < len(p.Commits)-1 {
  140. text += "\n"
  141. }
  142. }
  143. return d.createPayload(p.Sender, title, text, titleLink, greenColor), nil
  144. }
  145. // Issue implements PayloadConvertor Issue method
  146. func (d *DiscordPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
  147. title, _, text, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
  148. return d.createPayload(p.Sender, title, text, p.Issue.HTMLURL, color), nil
  149. }
  150. // IssueComment implements PayloadConvertor IssueComment method
  151. func (d *DiscordPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
  152. title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
  153. return d.createPayload(p.Sender, title, p.Comment.Body, p.Comment.HTMLURL, color), nil
  154. }
  155. // PullRequest implements PayloadConvertor PullRequest method
  156. func (d *DiscordPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
  157. title, _, text, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
  158. return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil
  159. }
  160. // Review implements PayloadConvertor Review method
  161. func (d *DiscordPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
  162. var text, title string
  163. var color int
  164. switch p.Action {
  165. case api.HookIssueReviewed:
  166. action, err := parseHookPullRequestEventType(event)
  167. if err != nil {
  168. return nil, err
  169. }
  170. title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
  171. text = p.Review.Content
  172. switch event {
  173. case webhook_module.HookEventPullRequestReviewApproved:
  174. color = greenColor
  175. case webhook_module.HookEventPullRequestReviewRejected:
  176. color = redColor
  177. case webhook_module.HookEventPullRequestReviewComment:
  178. color = greyColor
  179. default:
  180. color = yellowColor
  181. }
  182. }
  183. return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil
  184. }
  185. // Repository implements PayloadConvertor Repository method
  186. func (d *DiscordPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
  187. var title, url string
  188. var color int
  189. switch p.Action {
  190. case api.HookRepoCreated:
  191. title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
  192. url = p.Repository.HTMLURL
  193. color = greenColor
  194. case api.HookRepoDeleted:
  195. title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
  196. color = redColor
  197. }
  198. return d.createPayload(p.Sender, title, "", url, color), nil
  199. }
  200. // Wiki implements PayloadConvertor Wiki method
  201. func (d *DiscordPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
  202. text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
  203. htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
  204. var description string
  205. if p.Action != api.HookWikiDeleted {
  206. description = p.Comment
  207. }
  208. return d.createPayload(p.Sender, text, description, htmlLink, color), nil
  209. }
  210. // Release implements PayloadConvertor Release method
  211. func (d *DiscordPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
  212. text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
  213. return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
  214. }
  215. func (d *DiscordPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
  216. text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
  217. return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
  218. }
  219. // GetDiscordPayload converts a discord webhook into a DiscordPayload
  220. func GetDiscordPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
  221. s := new(DiscordPayload)
  222. discord := &DiscordMeta{}
  223. if err := json.Unmarshal([]byte(meta), &discord); err != nil {
  224. return s, errors.New("GetDiscordPayload meta json:" + err.Error())
  225. }
  226. s.Username = discord.Username
  227. s.AvatarURL = discord.IconURL
  228. return convertPayloader(s, p, event)
  229. }
  230. func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
  231. switch event {
  232. case webhook_module.HookEventPullRequestReviewApproved:
  233. return "approved", nil
  234. case webhook_module.HookEventPullRequestReviewRejected:
  235. return "rejected", nil
  236. case webhook_module.HookEventPullRequestReviewComment:
  237. return "comment", nil
  238. default:
  239. return "", errors.New("unknown event type")
  240. }
  241. }
  242. func (d *DiscordPayload) createPayload(s *api.User, title, text, url string, color int) *DiscordPayload {
  243. return &DiscordPayload{
  244. Username: d.Username,
  245. AvatarURL: d.AvatarURL,
  246. Embeds: []DiscordEmbed{
  247. {
  248. Title: title,
  249. Description: text,
  250. URL: url,
  251. Color: color,
  252. Author: DiscordEmbedAuthor{
  253. Name: s.UserName,
  254. URL: setting.AppURL + s.UserName,
  255. IconURL: s.AvatarURL,
  256. },
  257. },
  258. },
  259. }
  260. }