gitea源码

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. // Copyright 2017 The Gitea Authors. All rights reserved.
  2. // SPDX-License-Identifier: MIT
  3. package webhook
  4. import (
  5. "context"
  6. "errors"
  7. "fmt"
  8. "net/http"
  9. "net/url"
  10. "strconv"
  11. "strings"
  12. "unicode/utf8"
  13. webhook_model "code.gitea.io/gitea/models/webhook"
  14. "code.gitea.io/gitea/modules/git"
  15. "code.gitea.io/gitea/modules/json"
  16. "code.gitea.io/gitea/modules/log"
  17. "code.gitea.io/gitea/modules/setting"
  18. api "code.gitea.io/gitea/modules/structs"
  19. "code.gitea.io/gitea/modules/util"
  20. webhook_module "code.gitea.io/gitea/modules/webhook"
  21. )
  22. type (
  23. // DiscordEmbedFooter for Embed Footer Structure.
  24. DiscordEmbedFooter struct {
  25. Text string `json:"text"`
  26. }
  27. // DiscordEmbedAuthor for Embed Author Structure
  28. DiscordEmbedAuthor struct {
  29. Name string `json:"name"`
  30. URL string `json:"url"`
  31. IconURL string `json:"icon_url"`
  32. }
  33. // DiscordEmbedField for Embed Field Structure
  34. DiscordEmbedField struct {
  35. Name string `json:"name"`
  36. Value string `json:"value"`
  37. }
  38. // DiscordEmbed is for Embed Structure
  39. DiscordEmbed struct {
  40. Title string `json:"title"`
  41. Description string `json:"description"`
  42. URL string `json:"url"`
  43. Color int `json:"color"`
  44. Footer DiscordEmbedFooter `json:"footer"`
  45. Author DiscordEmbedAuthor `json:"author"`
  46. Fields []DiscordEmbedField `json:"fields"`
  47. }
  48. // DiscordPayload represents
  49. DiscordPayload struct {
  50. Wait bool `json:"wait"`
  51. Content string `json:"content"`
  52. Username string `json:"username,omitempty"`
  53. AvatarURL string `json:"avatar_url,omitempty"`
  54. TTS bool `json:"tts"`
  55. Embeds []DiscordEmbed `json:"embeds"`
  56. }
  57. // DiscordMeta contains the discord metadata
  58. DiscordMeta struct {
  59. Username string `json:"username"`
  60. IconURL string `json:"icon_url"`
  61. }
  62. )
  63. // GetDiscordHook returns discord metadata
  64. func GetDiscordHook(w *webhook_model.Webhook) *DiscordMeta {
  65. s := &DiscordMeta{}
  66. if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
  67. log.Error("webhook.GetDiscordHook(%d): %v", w.ID, err)
  68. }
  69. return s
  70. }
  71. func color(clr string) int {
  72. if clr != "" {
  73. clr = strings.TrimLeft(clr, "#")
  74. if s, err := strconv.ParseInt(clr, 16, 32); err == nil {
  75. return int(s)
  76. }
  77. }
  78. return 0
  79. }
  80. var (
  81. greenColor = color("1ac600")
  82. greenColorLight = color("bfe5bf")
  83. yellowColor = color("ffd930")
  84. greyColor = color("4f545c")
  85. purpleColor = color("7289da")
  86. orangeColor = color("eb6420")
  87. orangeColorLight = color("e68d60")
  88. redColor = color("ff3232")
  89. )
  90. // https://discord.com/developers/docs/resources/message#embed-object-embed-limits
  91. // Discord has some limits in place for the embeds.
  92. // According to some tests, there is no consistent limit for different character sets.
  93. // For example: 4096 ASCII letters are allowed, but only 2490 emoji characters are allowed.
  94. // To keep it simple, we currently truncate at 2000.
  95. const discordDescriptionCharactersLimit = 2000
  96. type discordConvertor struct {
  97. Username string
  98. AvatarURL string
  99. }
  100. // Create implements PayloadConvertor Create method
  101. func (d discordConvertor) Create(p *api.CreatePayload) (DiscordPayload, error) {
  102. // created tag/branch
  103. refName := git.RefName(p.Ref).ShortName()
  104. title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
  105. return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), greenColor), nil
  106. }
  107. // Delete implements PayloadConvertor Delete method
  108. func (d discordConvertor) Delete(p *api.DeletePayload) (DiscordPayload, error) {
  109. // deleted tag/branch
  110. refName := git.RefName(p.Ref).ShortName()
  111. title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
  112. return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName), redColor), nil
  113. }
  114. // Fork implements PayloadConvertor Fork method
  115. func (d discordConvertor) Fork(p *api.ForkPayload) (DiscordPayload, error) {
  116. title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
  117. return d.createPayload(p.Sender, title, "", p.Repo.HTMLURL, greenColor), nil
  118. }
  119. // Push implements PayloadConvertor Push method
  120. func (d discordConvertor) Push(p *api.PushPayload) (DiscordPayload, error) {
  121. var (
  122. branchName = git.RefName(p.Ref).ShortName()
  123. commitDesc string
  124. )
  125. var titleLink string
  126. if p.TotalCommits == 1 {
  127. commitDesc = "1 new commit"
  128. titleLink = p.Commits[0].URL
  129. } else {
  130. commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
  131. titleLink = p.CompareURL
  132. }
  133. if titleLink == "" {
  134. titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName)
  135. }
  136. title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
  137. var text string
  138. // for each commit, generate attachment text
  139. for i, commit := range p.Commits {
  140. // limit the commit message display to just the summary, otherwise it would be hard to read
  141. message := strings.TrimRight(strings.SplitN(commit.Message, "\n", 2)[0], "\r")
  142. // a limit of 50 is set because GitHub does the same
  143. if utf8.RuneCountInString(message) > 50 {
  144. message = fmt.Sprintf("%.47s...", message)
  145. }
  146. text += fmt.Sprintf("[%s](%s) %s - %s", commit.ID[:7], commit.URL, message, commit.Author.Name)
  147. // add linebreak to each commit but the last
  148. if i < len(p.Commits)-1 {
  149. text += "\n"
  150. }
  151. }
  152. return d.createPayload(p.Sender, title, text, titleLink, greenColor), nil
  153. }
  154. // Issue implements PayloadConvertor Issue method
  155. func (d discordConvertor) Issue(p *api.IssuePayload) (DiscordPayload, error) {
  156. title, _, extraMarkdown, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
  157. return d.createPayload(p.Sender, title, extraMarkdown, p.Issue.HTMLURL, color), nil
  158. }
  159. // IssueComment implements PayloadConvertor IssueComment method
  160. func (d discordConvertor) IssueComment(p *api.IssueCommentPayload) (DiscordPayload, error) {
  161. title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
  162. return d.createPayload(p.Sender, title, p.Comment.Body, p.Comment.HTMLURL, color), nil
  163. }
  164. // PullRequest implements PayloadConvertor PullRequest method
  165. func (d discordConvertor) PullRequest(p *api.PullRequestPayload) (DiscordPayload, error) {
  166. title, _, extraMarkdown, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
  167. return d.createPayload(p.Sender, title, extraMarkdown, p.PullRequest.HTMLURL, color), nil
  168. }
  169. // Review implements PayloadConvertor Review method
  170. func (d discordConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DiscordPayload, error) {
  171. var text, title string
  172. var color int
  173. switch p.Action {
  174. case api.HookIssueReviewed:
  175. action, err := parseHookPullRequestEventType(event)
  176. if err != nil {
  177. return DiscordPayload{}, err
  178. }
  179. title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
  180. text = p.Review.Content
  181. switch event {
  182. case webhook_module.HookEventPullRequestReviewApproved:
  183. color = greenColor
  184. case webhook_module.HookEventPullRequestReviewRejected:
  185. color = redColor
  186. case webhook_module.HookEventPullRequestReviewComment:
  187. color = greyColor
  188. default:
  189. color = yellowColor
  190. }
  191. }
  192. return d.createPayload(p.Sender, title, text, p.PullRequest.HTMLURL, color), nil
  193. }
  194. // Repository implements PayloadConvertor Repository method
  195. func (d discordConvertor) Repository(p *api.RepositoryPayload) (DiscordPayload, error) {
  196. var title, url string
  197. var color int
  198. switch p.Action {
  199. case api.HookRepoCreated:
  200. title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
  201. url = p.Repository.HTMLURL
  202. color = greenColor
  203. case api.HookRepoDeleted:
  204. title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
  205. color = redColor
  206. }
  207. return d.createPayload(p.Sender, title, "", url, color), nil
  208. }
  209. // Wiki implements PayloadConvertor Wiki method
  210. func (d discordConvertor) Wiki(p *api.WikiPayload) (DiscordPayload, error) {
  211. text, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
  212. htmlLink := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
  213. var description string
  214. if p.Action != api.HookWikiDeleted {
  215. description = p.Comment
  216. }
  217. return d.createPayload(p.Sender, text, description, htmlLink, color), nil
  218. }
  219. // Release implements PayloadConvertor Release method
  220. func (d discordConvertor) Release(p *api.ReleasePayload) (DiscordPayload, error) {
  221. text, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
  222. return d.createPayload(p.Sender, text, p.Release.Note, p.Release.HTMLURL, color), nil
  223. }
  224. func (d discordConvertor) Package(p *api.PackagePayload) (DiscordPayload, error) {
  225. text, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
  226. return d.createPayload(p.Sender, text, "", p.Package.HTMLURL, color), nil
  227. }
  228. func (d discordConvertor) Status(p *api.CommitStatusPayload) (DiscordPayload, error) {
  229. text, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
  230. return d.createPayload(p.Sender, text, "", p.TargetURL, color), nil
  231. }
  232. func (d discordConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DiscordPayload, error) {
  233. text, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
  234. return d.createPayload(p.Sender, text, "", p.WorkflowRun.HTMLURL, color), nil
  235. }
  236. func (d discordConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DiscordPayload, error) {
  237. text, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
  238. return d.createPayload(p.Sender, text, "", p.WorkflowJob.HTMLURL, color), nil
  239. }
  240. func newDiscordRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
  241. meta := &DiscordMeta{}
  242. if err := json.Unmarshal([]byte(w.Meta), meta); err != nil {
  243. return nil, nil, fmt.Errorf("newDiscordRequest meta json: %w", err)
  244. }
  245. var pc payloadConvertor[DiscordPayload] = discordConvertor{
  246. Username: meta.Username,
  247. AvatarURL: meta.IconURL,
  248. }
  249. return newJSONRequest(pc, w, t, true)
  250. }
  251. func init() {
  252. RegisterWebhookRequester(webhook_module.DISCORD, newDiscordRequest)
  253. }
  254. func parseHookPullRequestEventType(event webhook_module.HookEventType) (string, error) {
  255. switch event {
  256. case webhook_module.HookEventPullRequestReviewApproved:
  257. return "approved", nil
  258. case webhook_module.HookEventPullRequestReviewRejected:
  259. return "requested changes", nil
  260. case webhook_module.HookEventPullRequestReviewComment:
  261. return "comment", nil
  262. default:
  263. return "", errors.New("unknown event type")
  264. }
  265. }
  266. func (d discordConvertor) createPayload(s *api.User, title, text, url string, color int) DiscordPayload {
  267. return DiscordPayload{
  268. Username: d.Username,
  269. AvatarURL: d.AvatarURL,
  270. Embeds: []DiscordEmbed{
  271. {
  272. Title: title,
  273. Description: util.TruncateRunes(text, discordDescriptionCharactersLimit),
  274. URL: url,
  275. Color: color,
  276. Author: DiscordEmbedAuthor{
  277. Name: s.UserName,
  278. URL: setting.AppURL + s.UserName,
  279. IconURL: s.AvatarURL,
  280. },
  281. },
  282. },
  283. }
  284. }