Files
gitpm/internal/api/handlers/webhook.go
huxunan 885fad6c64 Initial commit: Gitea Project Management System
Features:
- Complete project management system with Epic/Story/Task hierarchy
- Vue.js 3 + Element Plus frontend with kanban board
- Go backend with Gin framework and GORM
- OAuth2 integration with Gitea
- Docker containerization with MySQL
- RESTful API for project, task, and user management
- JWT authentication and authorization
- Responsive web interface with dashboard
2025-09-22 14:53:53 +08:00

218 lines
5.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handlers
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"strconv"
"strings"
"giteapm/internal/models"
"github.com/gin-gonic/gin"
)
type GiteaWebhookPayload struct {
Action string `json:"action"`
Number int `json:"number"`
Repository struct {
ID int `json:"id"`
Name string `json:"name"`
FullName string `json:"full_name"`
} `json:"repository"`
PullRequest *struct {
ID int `json:"id"`
Number int `json:"number"`
Title string `json:"title"`
State string `json:"state"`
HTMLURL string `json:"html_url"`
} `json:"pull_request,omitempty"`
Commits []struct {
ID string `json:"id"`
Message string `json:"message"`
URL string `json:"url"`
} `json:"commits,omitempty"`
Pusher struct {
ID int `json:"id"`
Username string `json:"username"`
} `json:"pusher,omitempty"`
}
func (h *Handlers) HandleGiteaWebhook(c *gin.Context) {
// 验证 webhook 签名
if !h.verifyWebhookSignature(c) {
c.JSON(http.StatusUnauthorized, ErrorResponse(401, "Invalid webhook signature"))
return
}
// 读取请求体
body, err := io.ReadAll(c.Request.Body)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse(400, "Failed to read request body"))
return
}
// 解析 payload
var payload GiteaWebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse(400, "Invalid JSON payload"))
return
}
// 获取事件类型
eventType := c.GetHeader("X-Gitea-Event")
// 处理不同类型的事件
switch eventType {
case "push":
h.handlePushEvent(payload)
case "pull_request":
h.handlePullRequestEvent(payload)
case "issues":
h.handleIssueEvent(payload)
default:
// 忽略其他事件类型
}
c.JSON(http.StatusOK, SuccessResponse(gin.H{"message": "Webhook processed successfully"}))
}
func (h *Handlers) verifyWebhookSignature(c *gin.Context) bool {
signature := c.GetHeader("X-Gitea-Signature")
if signature == "" {
return false
}
// 读取请求体
body, err := io.ReadAll(c.Request.Body)
if err != nil {
return false
}
// 重新设置请求体,以便后续读取
c.Request.Body = io.NopCloser(strings.NewReader(string(body)))
// 计算 HMAC-SHA256
mac := hmac.New(sha256.New, []byte(h.Cfg.Gitea.WebhookSecret))
mac.Write(body)
expectedSignature := hex.EncodeToString(mac.Sum(nil))
// 比较签名
return hmac.Equal([]byte(signature), []byte(expectedSignature))
}
func (h *Handlers) handlePushEvent(payload GiteaWebhookPayload) {
// 处理推送事件
repoID := uint(payload.Repository.ID)
for _, commit := range payload.Commits {
// 查找关联的任务
_, err := models.GetGiteaRelationsByRepo(repoID, "commit")
if err != nil {
continue
}
// 检查提交消息中是否包含任务引用(如 #123
commitMessage := commit.Message
taskNumbers := extractTaskNumbers(commitMessage)
for _, taskNumber := range taskNumbers {
// 创建或更新 Git 关联
relation := &models.GiteaRelation{
GiteaRepoID: repoID,
GiteaRepoName: payload.Repository.FullName,
RelationType: "commit",
GiteaObjectID: commit.ID,
GiteaObjectTitle: commitMessage,
GiteaObjectURL: commit.URL,
}
// 根据任务号查找任务
if task := findTaskByNumber(taskNumber); task != nil {
relation.TaskID = task.ID
models.CreateGiteaRelation(relation)
// 可以根据提交消息自动更新任务状态
if strings.Contains(strings.ToLower(commitMessage), "fix") ||
strings.Contains(strings.ToLower(commitMessage), "resolve") {
task.Status = "review"
models.UpdateTask(task)
}
}
}
}
}
func (h *Handlers) handlePullRequestEvent(payload GiteaWebhookPayload) {
if payload.PullRequest == nil {
return
}
pr := payload.PullRequest
repoID := uint(payload.Repository.ID)
// 检查 PR 标题中的任务引用
taskNumbers := extractTaskNumbers(pr.Title)
for _, taskNumber := range taskNumbers {
if task := findTaskByNumber(taskNumber); task != nil {
relation := &models.GiteaRelation{
TaskID: task.ID,
GiteaRepoID: repoID,
GiteaRepoName: payload.Repository.FullName,
RelationType: "pull_request",
GiteaObjectID: strconv.Itoa(pr.ID),
GiteaObjectNumber: func() *uint { n := uint(pr.Number); return &n }(),
GiteaObjectTitle: pr.Title,
GiteaObjectURL: pr.HTMLURL,
}
models.CreateGiteaRelation(relation)
// 根据 PR 状态自动更新任务状态
switch payload.Action {
case "opened":
task.Status = "review"
case "closed":
if pr.State == "merged" {
task.Status = "done"
}
}
models.UpdateTask(task)
}
}
}
func (h *Handlers) handleIssueEvent(payload GiteaWebhookPayload) {
// 处理 Issue 事件,可以将 Gitea Issue 同步为任务
// 这里可以根据需要实现
}
// 从文本中提取任务编号(如 #123, fixes #456
func extractTaskNumbers(text string) []int {
var numbers []int
// 简单的正则匹配,实际可以使用更复杂的逻辑
words := strings.Fields(text)
for _, word := range words {
if strings.HasPrefix(word, "#") {
if num, err := strconv.Atoi(strings.TrimPrefix(word, "#")); err == nil {
numbers = append(numbers, num)
}
}
}
return numbers
}
// 根据任务编号查找任务(这里简化实现)
func findTaskByNumber(taskNumber int) *models.Task {
// 这里应该实现根据任务编号查找任务的逻辑
// 可以在数据库中添加 task_number 字段,或者使用 ID
task, err := models.GetTaskByID(uint(taskNumber))
if err != nil {
return nil
}
return task
}