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  }