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