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]>
213 lines
7.5 KiB
Go
213 lines
7.5 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
webhook_model "github.com/hanzoai/git/models/webhook"
|
|
"github.com/hanzoai/git/modules/git"
|
|
api "github.com/hanzoai/git/modules/structs"
|
|
"github.com/hanzoai/git/modules/util"
|
|
webhook_module "github.com/hanzoai/git/modules/webhook"
|
|
)
|
|
|
|
type (
|
|
DingtalkPayload DingtalkPayloadMsg
|
|
dingtalkConvertor struct{}
|
|
)
|
|
|
|
// Create implements PayloadConvertor Create method
|
|
func (dc dingtalkConvertor) Create(p *api.CreatePayload) (DingtalkPayload, error) {
|
|
// created tag/branch
|
|
refName := git.RefName(p.Ref).ShortName()
|
|
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
|
|
|
|
return createDingtalkPayload(title, title, "view ref "+refName, p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil
|
|
}
|
|
|
|
// Delete implements PayloadConvertor Delete method
|
|
func (dc dingtalkConvertor) Delete(p *api.DeletePayload) (DingtalkPayload, error) {
|
|
// created tag/branch
|
|
refName := git.RefName(p.Ref).ShortName()
|
|
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
|
|
|
|
return createDingtalkPayload(title, title, "view ref "+refName, p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName)), nil
|
|
}
|
|
|
|
// Fork implements PayloadConvertor Fork method
|
|
func (dc dingtalkConvertor) Fork(p *api.ForkPayload) (DingtalkPayload, error) {
|
|
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
|
|
|
|
return createDingtalkPayload(title, title, "view forked repo "+p.Repo.FullName, p.Repo.HTMLURL), nil
|
|
}
|
|
|
|
// Push implements PayloadConvertor Push method
|
|
func (dc dingtalkConvertor) Push(p *api.PushPayload) (DingtalkPayload, error) {
|
|
var (
|
|
branchName = git.RefName(p.Ref).ShortName()
|
|
commitDesc string
|
|
)
|
|
|
|
var titleLink, linkText string
|
|
if p.TotalCommits == 1 {
|
|
commitDesc = "1 new commit"
|
|
titleLink = p.Commits[0].URL
|
|
linkText = "view commit"
|
|
} else {
|
|
commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
|
|
titleLink = p.CompareURL
|
|
linkText = "view commits"
|
|
}
|
|
if titleLink == "" {
|
|
titleLink = p.Repo.HTMLURL + "/src/" + util.PathEscapeSegments(branchName)
|
|
}
|
|
|
|
title := fmt.Sprintf("[%s:%s] %s", p.Repo.FullName, branchName, commitDesc)
|
|
|
|
var text strings.Builder
|
|
// 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 createDingtalkPayload(title, text.String(), linkText, titleLink), nil
|
|
}
|
|
|
|
// Issue implements PayloadConvertor Issue method
|
|
func (dc dingtalkConvertor) Issue(p *api.IssuePayload) (DingtalkPayload, error) {
|
|
text, issueTitle, extraMarkdown, _ := getIssuesPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+extraMarkdown, "view issue", p.Issue.HTMLURL), nil
|
|
}
|
|
|
|
// Wiki implements PayloadConvertor Wiki method
|
|
func (dc dingtalkConvertor) Wiki(p *api.WikiPayload) (DingtalkPayload, error) {
|
|
text, _, _ := getWikiPayloadInfo(p, noneLinkFormatter, true)
|
|
url := p.Repository.HTMLURL + "/wiki/" + url.PathEscape(p.Page)
|
|
|
|
return createDingtalkPayload(text, text, "view wiki", url), nil
|
|
}
|
|
|
|
// IssueComment implements PayloadConvertor IssueComment method
|
|
func (dc dingtalkConvertor) IssueComment(p *api.IssueCommentPayload) (DingtalkPayload, error) {
|
|
text, issueTitle, _ := getIssueCommentPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+p.Comment.Body, "view issue comment", p.Comment.HTMLURL), nil
|
|
}
|
|
|
|
// PullRequest implements PayloadConvertor PullRequest method
|
|
func (dc dingtalkConvertor) PullRequest(p *api.PullRequestPayload) (DingtalkPayload, error) {
|
|
text, issueTitle, extraMarkdown, _ := getPullRequestPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return createDingtalkPayload(issueTitle, text+"\r\n\r\n"+extraMarkdown, "view pull request", p.PullRequest.HTMLURL), nil
|
|
}
|
|
|
|
// Review implements PayloadConvertor Review method
|
|
func (dc dingtalkConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (DingtalkPayload, error) {
|
|
var text, title string
|
|
switch p.Action {
|
|
case api.HookIssueReviewed:
|
|
action, err := parseHookPullRequestEventType(event)
|
|
if err != nil {
|
|
return DingtalkPayload{}, 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 createDingtalkPayload(title, title+"\r\n\r\n"+text, "view pull request", p.PullRequest.HTMLURL), nil
|
|
}
|
|
|
|
// Repository implements PayloadConvertor Repository method
|
|
func (dc dingtalkConvertor) Repository(p *api.RepositoryPayload) (DingtalkPayload, error) {
|
|
switch p.Action {
|
|
case api.HookRepoCreated:
|
|
title := fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
|
|
return createDingtalkPayload(title, title, "view repository", p.Repository.HTMLURL), nil
|
|
case api.HookRepoDeleted:
|
|
title := fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
|
return DingtalkPayload{
|
|
MsgType: "text",
|
|
Text: struct {
|
|
Content string `json:"content"`
|
|
}{
|
|
Content: title,
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return DingtalkPayload{}, nil
|
|
}
|
|
|
|
// Release implements PayloadConvertor Release method
|
|
func (dc dingtalkConvertor) Release(p *api.ReleasePayload) (DingtalkPayload, error) {
|
|
text, _ := getReleasePayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return createDingtalkPayload(text, text, "view release", p.Release.HTMLURL), nil
|
|
}
|
|
|
|
func (dc dingtalkConvertor) Package(p *api.PackagePayload) (DingtalkPayload, error) {
|
|
text, _ := getPackagePayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return createDingtalkPayload(text, text, "view package", p.Package.HTMLURL), nil
|
|
}
|
|
|
|
func (dc dingtalkConvertor) Status(p *api.CommitStatusPayload) (DingtalkPayload, error) {
|
|
text, _ := getStatusPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return createDingtalkPayload(text, text, "Status Changed", p.TargetURL), nil
|
|
}
|
|
|
|
func (dingtalkConvertor) WorkflowRun(p *api.WorkflowRunPayload) (DingtalkPayload, error) {
|
|
text, _ := getWorkflowRunPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return createDingtalkPayload(text, text, "Workflow Run", p.WorkflowRun.HTMLURL), nil
|
|
}
|
|
|
|
func (dingtalkConvertor) WorkflowJob(p *api.WorkflowJobPayload) (DingtalkPayload, error) {
|
|
text, _ := getWorkflowJobPayloadInfo(p, noneLinkFormatter, true)
|
|
|
|
return createDingtalkPayload(text, text, "Workflow Job", p.WorkflowJob.HTMLURL), nil
|
|
}
|
|
|
|
func createDingtalkPayload(title, text, singleTitle, singleURL string) DingtalkPayload {
|
|
return DingtalkPayload{
|
|
MsgType: "actionCard",
|
|
ActionCard: DingtalkActionCard{
|
|
Text: strings.TrimSpace(text),
|
|
Title: strings.TrimSpace(title),
|
|
HideAvatar: "0",
|
|
SingleTitle: singleTitle,
|
|
|
|
// https://developers.dingtalk.com/document/app/message-link-description
|
|
// to open the link in browser, we should use this URL, otherwise the page is displayed inside DingTalk client, very difficult to visit non-public URLs.
|
|
SingleURL: "dingtalk://dingtalkclient/page/link?pc_slide=false&url=" + url.QueryEscape(singleURL),
|
|
},
|
|
}
|
|
}
|
|
|
|
func newDingtalkRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
|
var pc payloadConvertor[DingtalkPayload] = dingtalkConvertor{}
|
|
return newJSONRequest(pc, w, t, true)
|
|
}
|
|
|
|
func init() {
|
|
RegisterWebhookRequester(webhook_module.DINGTALK, newDingtalkRequest)
|
|
}
|