Files
Gitea/services/webhook/feishu.go
T
roman.s 38b07d0c29 feat(webhook): fire repository event on repo rename (#38641)
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>
2026-07-31 11:31:17 +00:00

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)
}