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]>
412 lines
11 KiB
Go
412 lines
11 KiB
Go
// Copyright 2026 Hanzo AI, Inc. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package webhook
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"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 (
|
|
// MSTeamsFact for Fact Structure
|
|
MSTeamsFact struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
// MSTeamsSection is a MessageCard section
|
|
MSTeamsSection struct {
|
|
ActivityTitle string `json:"activityTitle"`
|
|
ActivitySubtitle string `json:"activitySubtitle"`
|
|
ActivityImage string `json:"activityImage"`
|
|
Facts []MSTeamsFact `json:"facts"`
|
|
Text string `json:"text"`
|
|
}
|
|
|
|
// MSTeamsAction is an action (creates buttons, links etc)
|
|
MSTeamsAction struct {
|
|
Type string `json:"@type"`
|
|
Name string `json:"name"`
|
|
Targets []MSTeamsActionTarget `json:"targets,omitempty"`
|
|
}
|
|
|
|
// MSTeamsActionTarget is the actual link to follow, etc
|
|
MSTeamsActionTarget struct {
|
|
Os string `json:"os"`
|
|
URI string `json:"uri"`
|
|
}
|
|
|
|
// MSTeamsPayload is the parent object
|
|
MSTeamsPayload struct {
|
|
Type string `json:"@type"`
|
|
Context string `json:"@context"`
|
|
ThemeColor string `json:"themeColor"`
|
|
Title string `json:"title"`
|
|
Summary string `json:"summary"`
|
|
Sections []MSTeamsSection `json:"sections"`
|
|
PotentialAction []MSTeamsAction `json:"potentialAction"`
|
|
}
|
|
)
|
|
|
|
type msteamsConvertor struct{}
|
|
|
|
// Create implements PayloadConvertor Create method
|
|
func (m msteamsConvertor) Create(p *api.CreatePayload) (MSTeamsPayload, error) {
|
|
// created tag/branch
|
|
refName := git.RefName(p.Ref).ShortName()
|
|
title := fmt.Sprintf("[%s] %s %s created", p.Repo.FullName, p.RefType, refName)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repo,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName),
|
|
greenColor,
|
|
&MSTeamsFact{p.RefType + ":", refName},
|
|
), nil
|
|
}
|
|
|
|
// Delete implements PayloadConvertor Delete method
|
|
func (m msteamsConvertor) Delete(p *api.DeletePayload) (MSTeamsPayload, error) {
|
|
// deleted tag/branch
|
|
refName := git.RefName(p.Ref).ShortName()
|
|
title := fmt.Sprintf("[%s] %s %s deleted", p.Repo.FullName, p.RefType, refName)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repo,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.Repo.HTMLURL+"/src/"+util.PathEscapeSegments(refName),
|
|
yellowColor,
|
|
&MSTeamsFact{p.RefType + ":", refName},
|
|
), nil
|
|
}
|
|
|
|
// Fork implements PayloadConvertor Fork method
|
|
func (m msteamsConvertor) Fork(p *api.ForkPayload) (MSTeamsPayload, error) {
|
|
title := fmt.Sprintf("%s is forked to %s", p.Forkee.FullName, p.Repo.FullName)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repo,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.Repo.HTMLURL,
|
|
greenColor,
|
|
&MSTeamsFact{"Forkee:", p.Forkee.FullName},
|
|
), nil
|
|
}
|
|
|
|
// Push implements PayloadConvertor Push method
|
|
func (m msteamsConvertor) Push(p *api.PushPayload) (MSTeamsPayload, error) {
|
|
var (
|
|
branchName = git.RefName(p.Ref).ShortName()
|
|
commitDesc string
|
|
)
|
|
|
|
var titleLink string
|
|
if p.TotalCommits == 1 {
|
|
commitDesc = "1 new commit"
|
|
titleLink = p.Commits[0].URL
|
|
} else {
|
|
commitDesc = fmt.Sprintf("%d new commits", p.TotalCommits)
|
|
titleLink = p.CompareURL
|
|
}
|
|
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 {
|
|
fmt.Fprintf(&text, "[%s](%s) %s - %s", commit.ID[:7], commit.URL,
|
|
strings.TrimRight(commit.Message, "\r\n"), commit.Author.Name)
|
|
// add linebreak to each commit but the last
|
|
if i < len(p.Commits)-1 {
|
|
text.WriteString("\n\n")
|
|
}
|
|
}
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repo,
|
|
p.Sender,
|
|
title,
|
|
text.String(),
|
|
titleLink,
|
|
greenColor,
|
|
&MSTeamsFact{"Commit count:", strconv.Itoa(p.TotalCommits)},
|
|
), nil
|
|
}
|
|
|
|
// Issue implements PayloadConvertor Issue method
|
|
func (m msteamsConvertor) Issue(p *api.IssuePayload) (MSTeamsPayload, error) {
|
|
title, _, extraMarkdown, color := getIssuesPayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repository,
|
|
p.Sender,
|
|
title,
|
|
extraMarkdown,
|
|
p.Issue.HTMLURL,
|
|
color,
|
|
&MSTeamsFact{"Issue #:", strconv.FormatInt(p.Issue.ID, 10)},
|
|
), nil
|
|
}
|
|
|
|
// IssueComment implements PayloadConvertor IssueComment method
|
|
func (m msteamsConvertor) IssueComment(p *api.IssueCommentPayload) (MSTeamsPayload, error) {
|
|
title, _, color := getIssueCommentPayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repository,
|
|
p.Sender,
|
|
title,
|
|
p.Comment.Body,
|
|
p.Comment.HTMLURL,
|
|
color,
|
|
&MSTeamsFact{"Issue #:", strconv.FormatInt(p.Issue.ID, 10)},
|
|
), nil
|
|
}
|
|
|
|
// PullRequest implements PayloadConvertor PullRequest method
|
|
func (m msteamsConvertor) PullRequest(p *api.PullRequestPayload) (MSTeamsPayload, error) {
|
|
title, _, extraMarkdown, color := getPullRequestPayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
facts := []*MSTeamsFact{
|
|
{"Pull request:", fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL)},
|
|
}
|
|
|
|
if (p.Action == api.HookIssueReviewRequested || p.Action == api.HookIssueReviewRequestRemoved) && p.RequestedReviewer != nil {
|
|
reviewerName := p.RequestedReviewer.UserName
|
|
if p.RequestedReviewer.FullName != "" {
|
|
reviewerName += " (" + p.RequestedReviewer.FullName + ")"
|
|
}
|
|
facts = append(facts, &MSTeamsFact{"Requested Reviewer:", reviewerName})
|
|
}
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repository,
|
|
p.Sender,
|
|
title,
|
|
extraMarkdown,
|
|
p.PullRequest.HTMLURL,
|
|
color,
|
|
facts...,
|
|
), nil
|
|
}
|
|
|
|
// Review implements PayloadConvertor Review method
|
|
func (m msteamsConvertor) Review(p *api.PullRequestPayload, event webhook_module.HookEventType) (MSTeamsPayload, error) {
|
|
var text, title string
|
|
var color int
|
|
switch p.Action {
|
|
case api.HookIssueReviewed:
|
|
action, err := parseHookPullRequestEventType(event)
|
|
if err != nil {
|
|
return MSTeamsPayload{}, err
|
|
}
|
|
|
|
title = fmt.Sprintf("[%s] Pull request review %s: #%d %s", p.Repository.FullName, action, p.Index, p.PullRequest.Title)
|
|
text = p.Review.Content
|
|
|
|
switch event {
|
|
case webhook_module.HookEventPullRequestReviewApproved:
|
|
color = greenColor
|
|
case webhook_module.HookEventPullRequestReviewRejected:
|
|
color = redColor
|
|
case webhook_module.HookEventPullRequestReviewComment:
|
|
color = greyColor
|
|
default:
|
|
color = yellowColor
|
|
}
|
|
}
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repository,
|
|
p.Sender,
|
|
title,
|
|
text,
|
|
p.PullRequest.HTMLURL,
|
|
color,
|
|
&MSTeamsFact{"Pull request:", fmt.Sprintf("[#%d](%s)", p.PullRequest.Index, p.PullRequest.HTMLURL)},
|
|
), nil
|
|
}
|
|
|
|
// Repository implements PayloadConvertor Repository method
|
|
func (m msteamsConvertor) Repository(p *api.RepositoryPayload) (MSTeamsPayload, error) {
|
|
var title, url string
|
|
var color int
|
|
switch p.Action {
|
|
case api.HookRepoCreated:
|
|
title = fmt.Sprintf("[%s] Repository created", p.Repository.FullName)
|
|
url = p.Repository.HTMLURL
|
|
color = greenColor
|
|
case api.HookRepoDeleted:
|
|
title = fmt.Sprintf("[%s] Repository deleted", p.Repository.FullName)
|
|
color = yellowColor
|
|
}
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repository,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
url,
|
|
color,
|
|
nil,
|
|
), nil
|
|
}
|
|
|
|
// Wiki implements PayloadConvertor Wiki method
|
|
func (m msteamsConvertor) Wiki(p *api.WikiPayload) (MSTeamsPayload, error) {
|
|
title, color, _ := getWikiPayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repository,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.Repository.HTMLURL+"/wiki/"+url.PathEscape(p.Page),
|
|
color,
|
|
), nil
|
|
}
|
|
|
|
// Release implements PayloadConvertor Release method
|
|
func (m msteamsConvertor) Release(p *api.ReleasePayload) (MSTeamsPayload, error) {
|
|
title, color := getReleasePayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repository,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.Release.HTMLURL,
|
|
color,
|
|
&MSTeamsFact{"Tag:", p.Release.TagName},
|
|
), nil
|
|
}
|
|
|
|
func (m msteamsConvertor) Package(p *api.PackagePayload) (MSTeamsPayload, error) {
|
|
title, color := getPackagePayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repository,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.Package.HTMLURL,
|
|
color,
|
|
&MSTeamsFact{"Package:", p.Package.Name},
|
|
), nil
|
|
}
|
|
|
|
func (m msteamsConvertor) Status(p *api.CommitStatusPayload) (MSTeamsPayload, error) {
|
|
title, color := getStatusPayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repo,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.TargetURL,
|
|
color,
|
|
&MSTeamsFact{"CommitStatus:", p.Context},
|
|
), nil
|
|
}
|
|
|
|
func (msteamsConvertor) WorkflowRun(p *api.WorkflowRunPayload) (MSTeamsPayload, error) {
|
|
title, color := getWorkflowRunPayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repo,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.WorkflowRun.HTMLURL,
|
|
color,
|
|
&MSTeamsFact{"WorkflowRun:", p.WorkflowRun.DisplayTitle},
|
|
), nil
|
|
}
|
|
|
|
func (msteamsConvertor) WorkflowJob(p *api.WorkflowJobPayload) (MSTeamsPayload, error) {
|
|
title, color := getWorkflowJobPayloadInfo(p, noneLinkFormatter, false)
|
|
|
|
return createMSTeamsPayload(
|
|
p.Repo,
|
|
p.Sender,
|
|
title,
|
|
"",
|
|
p.WorkflowJob.HTMLURL,
|
|
color,
|
|
&MSTeamsFact{"WorkflowJob:", p.WorkflowJob.Name},
|
|
), nil
|
|
}
|
|
|
|
func createMSTeamsPayload(r *api.Repository, s *api.User, title, text, actionTarget string, color int, extraFacts ...*MSTeamsFact) MSTeamsPayload {
|
|
facts := make([]MSTeamsFact, 0, len(extraFacts)+1)
|
|
if r != nil {
|
|
facts = append(facts, MSTeamsFact{
|
|
Name: "Repository:",
|
|
Value: fmt.Sprintf("[%s](%s)", r.FullName, r.HTMLURL),
|
|
})
|
|
}
|
|
for _, f := range extraFacts {
|
|
if f != nil {
|
|
facts = append(facts, *f)
|
|
}
|
|
}
|
|
|
|
return MSTeamsPayload{
|
|
Type: "MessageCard",
|
|
Context: "https://schema.org/extensions",
|
|
ThemeColor: fmt.Sprintf("%x", color),
|
|
Title: title,
|
|
Summary: title,
|
|
Sections: []MSTeamsSection{
|
|
{
|
|
ActivityTitle: s.FullName,
|
|
ActivitySubtitle: s.UserName,
|
|
ActivityImage: s.AvatarURL,
|
|
Text: text,
|
|
Facts: facts,
|
|
},
|
|
},
|
|
PotentialAction: []MSTeamsAction{
|
|
{
|
|
Type: "OpenUri",
|
|
Name: "View in Hanzo Git",
|
|
Targets: []MSTeamsActionTarget{
|
|
{
|
|
Os: "default",
|
|
URI: actionTarget,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func newMSTeamsRequest(_ context.Context, w *webhook_model.Webhook, t *webhook_model.HookTask) (*http.Request, []byte, error) {
|
|
var pc payloadConvertor[MSTeamsPayload] = msteamsConvertor{}
|
|
return newJSONRequest(pc, w, t, true)
|
|
}
|
|
|
|
func init() {
|
|
RegisterWebhookRequester(webhook_module.MSTEAMS, newMSTeamsRequest)
|
|
}
|