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 }