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  }