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