Files
forge/services/webhook/dingtalk.go
zeekayandHanzo Dev 06db83d323 webhook: define the DingTalk payload here, drop gitea.com/lunny/dingtalk_webhook
The dependency supplied four JSON struct definitions. Its other 361 lines are an
HTTP client that posts them, which this forge never called — every webhook goes
out through the shared deliverer with its own retries, proxy settings and rate
limits. A dependency existed to hold four structs.

They are now in services/webhook/dingtalk_payload.go next to the sender that
uses them, which is where the slack, discord, feishu, matrix and msteams shapes
already live. Field names and tags are DingTalk API, unchanged, so the wire
format is identical.

One gitea.com dependency left: gitea/runner, the Actions execution engine
(19808 LOC, 21 consumers) — a real upstream project, not middleware.

Webhook suite green; whole tree green.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 18:37:37 -07:00

214 lines
7.6 KiB
Go

// Copyright 2026 Hanzo AI, Inc. All rights reserved.
// Copyright 2017 The Gitea Authors. 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)
}