mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-01 13:19:03 +00:00
Repository create and delete already fire `repository` webhooks, rename did not. This adds the `renamed` action with `changes.name.from` carrying the previous name, and renders it in the chat converters. Actions workflows are unaffected, they still do not trigger on rename. AI assistance was used for the implementation and tests. Fixes https://github.com/go-gitea/gitea/issues/34891. Co-authored-by: roman s <roman.sukach@dust-labs.com> Co-authored-by: silverwind <me@silverwind.io>
225 lines
7.8 KiB
Go
225 lines
7.8 KiB
Go
// Copyright 2020 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
webhook_model "gitea.dev/models/webhook"
|
|
"gitea.dev/modules/git"
|
|
api "gitea.dev/modules/structs"
|
|
webhook_module "gitea.dev/modules/webhook"
|
|
)
|
|
|
|
type (
|
|
// FeishuPayload represents the payload for Feishu webhook
|
|
FeishuPayload struct {
|
|
Timestamp int64 `json:"timestamp,omitempty"` // Unix timestamp for signature verification
|
|
Sign string `json:"sign,omitempty"` // Signature for verification
|
|
MsgType string `json:"msg_type"` // text / post / image / share_chat / interactive / file /audio / media
|
|
Content struct {
|
|
Text string `json:"text"`
|
|
} `json:"content"`
|
|
}
|
|
)
|
|
|
|
func newFeishuTextPayload(text string) FeishuPayload {
|
|
return FeishuPayload{
|
|
MsgType: "text",
|
|
Content: struct {
|
|
Text string `json:"text"`
|
|
}{
|
|
Text: strings.TrimSpace(text),
|
|
},
|
|
}
|
|
}
|
|
|
|
type feishuConvertor struct{}
|
|
|
|
// Create implements PayloadConvertor Create method
|
|
func (fc feishuConvertor) Create(p *api.CreatePayload) (FeishuPayload, error) {
|
|
// created tag/branch
|
|
refName := git.RefName(p.Ref).ShortName()
|
|
text := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
// Delete implements PayloadConvertor Delete method
|
|
func (fc feishuConvertor) Delete(p *api.DeletePayload) (FeishuPayload, error) {
|
|
// created tag/branch
|
|
refName := git.RefName(p.Ref).ShortName()
|
|
text := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
// Fork implements PayloadConvertor Fork method
|
|
func (fc feishuConvertor) Fork(p *api.ForkPayload) (FeishuPayload, error) {
|
|
text := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
// Push implements PayloadConvertor Push method
|
|
func (fc feishuConvertor) Push(p *api.PushPayload) (FeishuPayload, error) {
|
|
var (
|
|
branchName = git.RefName(p.Ref).ShortName()
|
|
commitDesc string
|
|
)
|
|
|
|
var text strings.Builder
|
|
fmt.Fprintf(&text, "[%s:%s] %s\r\n", p.Repo.FullName, branchName, commitDesc)
|
|
// for each commit, generate attachment text
|
|
for i, commit := range p.Commits {
|
|
var authorName string
|
|
if commit.Author != nil {
|
|
authorName = " - " + commit.Author.Name
|
|
}
|
|
text.WriteString(fmt.Sprintf("[%s](%s) %s", commit.ID[:7], commit.URL,
|
|
strings.TrimRight(commit.Message, "\r\n")) + authorName)
|
|
// add linebreak to each commit but the last
|
|
if i < len(p.Commits)-1 {
|
|
text.WriteString("\r\n")
|
|
}
|
|
}
|
|
|
|
return newFeishuTextPayload(text.String()), nil
|
|
}
|
|
|
|
// Issue implements PayloadConvertor Issue method
|
|
func (fc feishuConvertor) Issue(p *api.IssuePayload) (FeishuPayload, error) {
|
|
title, link, by, operator, result, assignees := getIssuesInfo(p)
|
|
if assignees != "" {
|
|
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
|
|
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.Issue.Body)), nil
|
|
}
|
|
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.Issue.Body)), nil
|
|
}
|
|
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Issue.Body)), nil
|
|
}
|
|
|
|
// IssueComment implements PayloadConvertor IssueComment method
|
|
func (fc feishuConvertor) IssueComment(p *api.IssueCommentPayload) (FeishuPayload, error) {
|
|
title, link, by, operator := getIssuesCommentInfo(p)
|
|
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.Comment.Body)), nil
|
|
}
|
|
|
|
// PullRequest implements PayloadConvertor PullRequest method
|
|
func (fc feishuConvertor) PullRequest(p *api.PullRequestPayload) (FeishuPayload, error) {
|
|
title, link, by, operator, result, assignees := getPullRequestInfo(p)
|
|
if assignees != "" {
|
|
if p.Action == api.HookIssueAssigned || p.Action == api.HookIssueUnassigned || p.Action == api.HookIssueMilestoned {
|
|
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, result, assignees, p.PullRequest.Body)), nil
|
|
}
|
|
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, assignees, p.PullRequest.Body)), nil
|
|
}
|
|
return newFeishuTextPayload(fmt.Sprintf("%s\n%s\n%s\n%s\n\n%s", title, link, by, operator, p.PullRequest.Body)), nil
|
|
}
|
|
|
|
// Review implements PayloadConvertor Review method
|
|
func (fc feishuConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (FeishuPayload, error) {
|
|
action, err := parseHookPullRequestEventType(event)
|
|
if err != nil {
|
|
return FeishuPayload{}, err
|
|
}
|
|
|
|
title := fmt.Sprintf("[%s] Pull request review %s : #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
|
|
text := p.Review.Content
|
|
|
|
return newFeishuTextPayload(title + "\r\n\r\n" + text), nil
|
|
}
|
|
|
|
// Repository implements PayloadConvertor Repository method
|
|
func (fc feishuConvertor) Repository(p *api.RepositoryPayload) (FeishuPayload, error) {
|
|
var text string
|
|
switch p.Action {
|
|
case api.HookRepoCreated:
|
|
text = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
|
|
return newFeishuTextPayload(text), nil
|
|
case api.HookRepoDeleted:
|
|
text = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
|
return newFeishuTextPayload(text), nil
|
|
case api.HookRepoRenamed:
|
|
text = fmt.Sprintf("[%s] Repository renamed from %s", p.Repository.FullName, getRepoRenamedFrom(p))
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
return FeishuPayload{}, nil
|
|
}
|
|
|
|
// Wiki implements PayloadConvertor Wiki method
|
|
func (fc feishuConvertor) Wiki(p *api.WikiPayload) (FeishuPayload, error) {
|
|
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
// Release implements PayloadConvertor Release method
|
|
func (fc feishuConvertor) Release(p *api.ReleasePayload) (FeishuPayload, error) {
|
|
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
func (fc feishuConvertor) Package(p *api.PackagePayload) (FeishuPayload, error) {
|
|
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
func (fc feishuConvertor) Status(p *api.CommitStatusPayload) (FeishuPayload, error) {
|
|
text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
func (feishuConvertor) WorkflowRun(p *api.WorkflowRunPayload) (FeishuPayload, error) {
|
|
text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
func (feishuConvertor) WorkflowJob(p *api.WorkflowJobPayload) (FeishuPayload, error) {
|
|
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return newFeishuTextPayload(text), nil
|
|
}
|
|
|
|
// feishuGenSign generates a signature for Feishu webhook
|
|
// https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot
|
|
func feishuGenSign(secret string, timestamp int64) string {
|
|
// key="{timestamp}\n{secret}", then hmac-sha256, then base64 encode
|
|
stringToSign := fmt.Sprintf("%d\n%s", timestamp, secret)
|
|
h := hmac.New(sha256.New, []byte(stringToSign))
|
|
return base64.StdEncoding.EncodeToString(h.Sum(nil))
|
|
}
|
|
|
|
func newFeishuRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
|
payload, err := newPayload(feishuConvertor{}, []byte(t.PayloadContent), t.EventType)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Add timestamp and signature if secret is provided
|
|
if w.Secret != "" {
|
|
timestamp := time.Now().Unix()
|
|
payload.Timestamp = timestamp
|
|
payload.Sign = feishuGenSign(w.Secret, timestamp)
|
|
}
|
|
|
|
return prepareJSONRequest(payload, w, t, false /* no default headers */)
|
|
}
|
|
|
|
func init() {
|
|
RegisterWebhookRequester(webhook_module.FEISHU, newFeishuRequest)
|
|
}
|