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 }