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