mirror of
https://github.com/hanzoai/forge.git
synced 2026-10-30 15:00:39 +00:00
Source files keep their SPDX-License-Identifier and Hanzo copyright line; the Gitea, Gogs and LFS Test Server copyrights they used to repeat now live once in NOTICE, alongside LICENSE. The header lint accepts any copyright line above the SPDX line, so it no longer demands an upstream one. Co-authored-by: Hanzo Dev <[email protected]>
222 lines
7.7 KiB
Go
222 lines
7.7 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
webhook_model "github.com/hanzoai/git/models/webhook"
|
|
"github.com/hanzoai/git/modules/git"
|
|
api "github.com/hanzoai/git/modules/structs"
|
|
webhook_module "github.com/hanzoai/git/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
|
|
}
|
|
|
|
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)
|
|
}
|