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