code.gitea.io/gitea@v1.21.7/services/webhook/matrix.go (about)

     1  // Copyright 2020 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package webhook
     5  
     6  import (
     7  	"crypto/sha1"
     8  	"encoding/hex"
     9  	"errors"
    10  	"fmt"
    11  	"html"
    12  	"net/url"
    13  	"regexp"
    14  	"strings"
    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  const matrixPayloadSizeLimit = 1024 * 64
    27  
    28  // MatrixMeta contains the Matrix metadata
    29  type MatrixMeta struct {
    30  	HomeserverURL string `json:"homeserver_url"`
    31  	Room          string `json:"room_id"`
    32  	MessageType   int    `json:"message_type"`
    33  }
    34  
    35  var messageTypeText = map[int]string{
    36  	1: "m.notice",
    37  	2: "m.text",
    38  }
    39  
    40  // GetMatrixHook returns Matrix metadata
    41  func GetMatrixHook(w *webhook_model.Webhook) *MatrixMeta {
    42  	s := &MatrixMeta{}
    43  	if err := json.Unmarshal([]byte(w.Meta), s); err != nil {
    44  		log.Error("webhook.GetMatrixHook(%d): %v", w.ID, err)
    45  	}
    46  	return s
    47  }
    48  
    49  var _ PayloadConvertor = &MatrixPayload{}
    50  
    51  // MatrixPayload contains payload for a Matrix room
    52  type MatrixPayload struct {
    53  	Body          string               `json:"body"`
    54  	MsgType       string               `json:"msgtype"`
    55  	Format        string               `json:"format"`
    56  	FormattedBody string               `json:"formatted_body"`
    57  	Commits       []*api.PayloadCommit `json:"io.gitea.commits,omitempty"`
    58  }
    59  
    60  // JSONPayload Marshals the MatrixPayload to json
    61  func (m *MatrixPayload) JSONPayload() ([]byte, error) {
    62  	data, err := json.MarshalIndent(m, "", "  ")
    63  	if err != nil {
    64  		return []byte{}, err
    65  	}
    66  	return data, nil
    67  }
    68  
    69  // MatrixLinkFormatter creates a link compatible with Matrix
    70  func MatrixLinkFormatter(url, text string) string {
    71  	return fmt.Sprintf(`<a href="%s">%s</a>`, html.EscapeString(url), html.EscapeString(text))
    72  }
    73  
    74  // MatrixLinkToRef Matrix-formatter link to a repo ref
    75  func MatrixLinkToRef(repoURL, ref string) string {
    76  	refName := git.RefName(ref).ShortName()
    77  	switch {
    78  	case strings.HasPrefix(ref, git.BranchPrefix):
    79  		return MatrixLinkFormatter(repoURL+"/src/branch/"+util.PathEscapeSegments(refName), refName)
    80  	case strings.HasPrefix(ref, git.TagPrefix):
    81  		return MatrixLinkFormatter(repoURL+"/src/tag/"+util.PathEscapeSegments(refName), refName)
    82  	default:
    83  		return MatrixLinkFormatter(repoURL+"/src/commit/"+util.PathEscapeSegments(refName), refName)
    84  	}
    85  }
    86  
    87  // Create implements PayloadConvertor Create method
    88  func (m *MatrixPayload) Create(p *api.CreatePayload) (api.Payloader, error) {
    89  	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
    90  	refLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
    91  	text := fmt.Sprintf("[%s:%s] %s created by %s", repoLink, refLink, p.RefType, p.Sender.UserName)
    92  
    93  	return getMatrixPayload(text, nil, m.MsgType), nil
    94  }
    95  
    96  // Delete composes Matrix payload for delete a branch or tag.
    97  func (m *MatrixPayload) Delete(p *api.DeletePayload) (api.Payloader, error) {
    98  	refName := git.RefName(p.Ref).ShortName()
    99  	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
   100  	text := fmt.Sprintf("[%s:%s] %s deleted by %s", repoLink, refName, p.RefType, p.Sender.UserName)
   101  
   102  	return getMatrixPayload(text, nil, m.MsgType), nil
   103  }
   104  
   105  // Fork composes Matrix payload for forked by a repository.
   106  func (m *MatrixPayload) Fork(p *api.ForkPayload) (api.Payloader, error) {
   107  	baseLink := MatrixLinkFormatter(p.Forkee.HTMLURL, p.Forkee.FullName)
   108  	forkLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
   109  	text := fmt.Sprintf("%s is forked to %s", baseLink, forkLink)
   110  
   111  	return getMatrixPayload(text, nil, m.MsgType), nil
   112  }
   113  
   114  // Issue implements PayloadConvertor Issue method
   115  func (m *MatrixPayload) Issue(p *api.IssuePayload) (api.Payloader, error) {
   116  	text, _, _, _ := getIssuesPayloadInfo(p, MatrixLinkFormatter, true)
   117  
   118  	return getMatrixPayload(text, nil, m.MsgType), nil
   119  }
   120  
   121  // IssueComment implements PayloadConvertor IssueComment method
   122  func (m *MatrixPayload) IssueComment(p *api.IssueCommentPayload) (api.Payloader, error) {
   123  	text, _, _ := getIssueCommentPayloadInfo(p, MatrixLinkFormatter, true)
   124  
   125  	return getMatrixPayload(text, nil, m.MsgType), nil
   126  }
   127  
   128  // Wiki implements PayloadConvertor Wiki method
   129  func (m *MatrixPayload) Wiki(p *api.WikiPayload) (api.Payloader, error) {
   130  	text, _, _ := getWikiPayloadInfo(p, MatrixLinkFormatter, true)
   131  
   132  	return getMatrixPayload(text, nil, m.MsgType), nil
   133  }
   134  
   135  // Release implements PayloadConvertor Release method
   136  func (m *MatrixPayload) Release(p *api.ReleasePayload) (api.Payloader, error) {
   137  	text, _ := getReleasePayloadInfo(p, MatrixLinkFormatter, true)
   138  
   139  	return getMatrixPayload(text, nil, m.MsgType), nil
   140  }
   141  
   142  // Push implements PayloadConvertor Push method
   143  func (m *MatrixPayload) Push(p *api.PushPayload) (api.Payloader, error) {
   144  	var commitDesc string
   145  
   146  	if p.TotalCommits == 1 {
   147  		commitDesc = "1 commit"
   148  	} else {
   149  		commitDesc = fmt.Sprintf("%d commits", p.TotalCommits)
   150  	}
   151  
   152  	repoLink := MatrixLinkFormatter(p.Repo.HTMLURL, p.Repo.FullName)
   153  	branchLink := MatrixLinkToRef(p.Repo.HTMLURL, p.Ref)
   154  	text := fmt.Sprintf("[%s] %s pushed %s to %s:<br>", repoLink, p.Pusher.UserName, commitDesc, branchLink)
   155  
   156  	// for each commit, generate a new line text
   157  	for i, commit := range p.Commits {
   158  		text += fmt.Sprintf("%s: %s - %s", MatrixLinkFormatter(commit.URL, commit.ID[:7]), commit.Message, commit.Author.Name)
   159  		// add linebreak to each commit but the last
   160  		if i < len(p.Commits)-1 {
   161  			text += "<br>"
   162  		}
   163  
   164  	}
   165  
   166  	return getMatrixPayload(text, p.Commits, m.MsgType), nil
   167  }
   168  
   169  // PullRequest implements PayloadConvertor PullRequest method
   170  func (m *MatrixPayload) PullRequest(p *api.PullRequestPayload) (api.Payloader, error) {
   171  	text, _, _, _ := getPullRequestPayloadInfo(p, MatrixLinkFormatter, true)
   172  
   173  	return getMatrixPayload(text, nil, m.MsgType), nil
   174  }
   175  
   176  // Review implements PayloadConvertor Review method
   177  func (m *MatrixPayload) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (api.Payloader, error) {
   178  	senderLink := MatrixLinkFormatter(setting.AppURL+url.PathEscape(p.Sender.UserName), p.Sender.UserName)
   179  	title := fmt.Sprintf("#%d %s", p.Index, p.PullRequest.Title)
   180  	titleLink := MatrixLinkFormatter(p.PullRequest.HTMLURL, title)
   181  	repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
   182  	var text string
   183  
   184  	switch p.Action {
   185  	case api.HookIssueReviewed:
   186  		action, err := parseHookPullRequestEventType(event)
   187  		if err != nil {
   188  			return nil, err
   189  		}
   190  
   191  		text = fmt.Sprintf("[%s] Pull request review %s: %s by %s", repoLink, action, titleLink, senderLink)
   192  	}
   193  
   194  	return getMatrixPayload(text, nil, m.MsgType), nil
   195  }
   196  
   197  // Repository implements PayloadConvertor Repository method
   198  func (m *MatrixPayload) Repository(p *api.RepositoryPayload) (api.Payloader, error) {
   199  	senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
   200  	repoLink := MatrixLinkFormatter(p.Repository.HTMLURL, p.Repository.FullName)
   201  	var text string
   202  
   203  	switch p.Action {
   204  	case api.HookRepoCreated:
   205  		text = fmt.Sprintf("[%s] Repository created by %s", repoLink, senderLink)
   206  	case api.HookRepoDeleted:
   207  		text = fmt.Sprintf("[%s] Repository deleted by %s", repoLink, senderLink)
   208  	}
   209  
   210  	return getMatrixPayload(text, nil, m.MsgType), nil
   211  }
   212  
   213  func (m *MatrixPayload) Package(p *api.PackagePayload) (api.Payloader, error) {
   214  	senderLink := MatrixLinkFormatter(setting.AppURL+p.Sender.UserName, p.Sender.UserName)
   215  	packageLink := MatrixLinkFormatter(p.Package.HTMLURL, p.Package.Name)
   216  	var text string
   217  
   218  	switch p.Action {
   219  	case api.HookPackageCreated:
   220  		text = fmt.Sprintf("[%s] Package published by %s", packageLink, senderLink)
   221  	case api.HookPackageDeleted:
   222  		text = fmt.Sprintf("[%s] Package deleted by %s", packageLink, senderLink)
   223  	}
   224  
   225  	return getMatrixPayload(text, nil, m.MsgType), nil
   226  }
   227  
   228  // GetMatrixPayload converts a Matrix webhook into a MatrixPayload
   229  func GetMatrixPayload(p api.Payloader, event webhook_module.HookEventType, meta string) (api.Payloader, error) {
   230  	s := new(MatrixPayload)
   231  
   232  	matrix := &MatrixMeta{}
   233  	if err := json.Unmarshal([]byte(meta), &matrix); err != nil {
   234  		return s, errors.New("GetMatrixPayload meta json:" + err.Error())
   235  	}
   236  
   237  	s.MsgType = messageTypeText[matrix.MessageType]
   238  
   239  	return convertPayloader(s, p, event)
   240  }
   241  
   242  func getMatrixPayload(text string, commits []*api.PayloadCommit, msgType string) *MatrixPayload {
   243  	p := MatrixPayload{}
   244  	p.FormattedBody = text
   245  	p.Body = getMessageBody(text)
   246  	p.Format = "org.matrix.custom.html"
   247  	p.MsgType = msgType
   248  	p.Commits = commits
   249  	return &p
   250  }
   251  
   252  var urlRegex = regexp.MustCompile(`<a [^>]*?href="([^">]*?)">(.*?)</a>`)
   253  
   254  func getMessageBody(htmlText string) string {
   255  	htmlText = urlRegex.ReplaceAllString(htmlText, "[$2]($1)")
   256  	htmlText = strings.ReplaceAll(htmlText, "<br>", "\n")
   257  	return htmlText
   258  }
   259  
   260  // getMatrixTxnID computes the transaction ID to ensure idempotency
   261  func getMatrixTxnID(payload []byte) (string, error) {
   262  	if len(payload) >= matrixPayloadSizeLimit {
   263  		return "", fmt.Errorf("getMatrixTxnID: payload size %d > %d", len(payload), matrixPayloadSizeLimit)
   264  	}
   265  
   266  	h := sha1.New()
   267  	_, err := h.Write(payload)
   268  	if err != nil {
   269  		return "", err
   270  	}
   271  
   272  	return hex.EncodeToString(h.Sum(nil)), nil
   273  }