浏览代码

feat: webhook server part done (#76)

JustSong 2 年之前
父节点
当前提交
374400479f
共有 8 个文件被更改,包括 385 次插入1 次删除
  1. 6 0
      common/constants.go
  2. 5 1
      controller/message.go
  3. 256 0
      controller/webhook.go
  4. 3 0
      go.mod
  5. 7 0
      go.sum
  6. 4 0
      model/main.go
  7. 89 0
      model/webhook.go
  8. 15 0
      router/api-router.go

+ 6 - 0
common/constants.go

@@ -113,3 +113,9 @@ const (
 	ChannelStatusEnabled  = 1
 	ChannelStatusDisabled = 2
 )
+
+const (
+	WebhookStatusUnknown  = 0
+	WebhookStatusEnabled  = 1
+	WebhookStatusDisabled = 2
+)

+ 5 - 1
controller/message.go

@@ -116,6 +116,10 @@ func pushMessageHelper(c *gin.Context, message *model.Message) {
 			return
 		}
 	}
+	processMessage(c, message, &user)
+}
+
+func processMessage(c *gin.Context, message *model.Message, user *model.User) {
 	if message.Title == "" {
 		message.Title = common.SystemName
 	}
@@ -133,7 +137,7 @@ func pushMessageHelper(c *gin.Context, message *model.Message) {
 		})
 		return
 	}
-	err = saveAndSendMessage(&user, message, channel_)
+	err = saveAndSendMessage(user, message, channel_)
 	if err != nil {
 		c.JSON(http.StatusOK, gin.H{
 			"success": false,

+ 256 - 0
controller/webhook.go

@@ -0,0 +1,256 @@
+package controller
+
+import (
+	"encoding/json"
+	"github.com/gin-gonic/gin"
+	"github.com/tidwall/gjson"
+	"message-pusher/common"
+	"message-pusher/model"
+	"net/http"
+	"strconv"
+	"strings"
+)
+
+func GetAllWebhooks(c *gin.Context) {
+	userId := c.GetInt("id")
+	p, _ := strconv.Atoi(c.Query("p"))
+	if p < 0 {
+		p = 0
+	}
+	webhooks, err := model.GetWebhooksByUserId(userId, p*common.ItemsPerPage, common.ItemsPerPage)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    webhooks,
+	})
+	return
+}
+
+func SearchWebhooks(c *gin.Context) {
+	userId := c.GetInt("id")
+	keyword := c.Query("keyword")
+	webhooks, err := model.SearchWebhooks(userId, keyword)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    webhooks,
+	})
+	return
+}
+
+func GetWebhook(c *gin.Context) {
+	id, err := strconv.Atoi(c.Param("id"))
+	userId := c.GetInt("id")
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	webhook_, err := model.GetWebhookById(id, userId)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    webhook_,
+	})
+	return
+}
+
+func AddWebhook(c *gin.Context) {
+	webhook_ := model.Webhook{}
+	err := c.ShouldBindJSON(&webhook_)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	if len(webhook_.Name) == 0 || len(webhook_.Name) > 20 {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "通道名称长度必须在1-20之间",
+		})
+		return
+	}
+	cleanWebhook := model.Webhook{
+		UserId:      c.GetInt("id"),
+		Name:        webhook_.Name,
+		Status:      common.WebhookStatusEnabled,
+		Link:        common.GetUUID(),
+		CreatedTime: common.GetTimestamp(),
+		ExtractRule: webhook_.ExtractRule,
+	}
+	err = cleanWebhook.Insert()
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+	})
+	return
+}
+
+func DeleteWebhook(c *gin.Context) {
+	id, _ := strconv.Atoi(c.Param("id"))
+	userId := c.GetInt("id")
+	_, err := model.DeleteWebhookById(id, userId)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+	})
+	return
+}
+
+func UpdateWebhook(c *gin.Context) {
+	userId := c.GetInt("id")
+	statusOnly := c.Query("status_only")
+	webhook_ := model.Webhook{}
+	err := c.ShouldBindJSON(&webhook_)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	oldWebhook, err := model.GetWebhookById(webhook_.Id, userId)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	cleanWebhook := *oldWebhook
+	if statusOnly != "" {
+		cleanWebhook.Status = webhook_.Status
+	} else {
+		// If you add more fields, please also update webhook_.Update()
+		cleanWebhook.Name = webhook_.Name
+		cleanWebhook.ExtractRule = webhook_.ExtractRule
+		cleanWebhook.ConstructRule = webhook_.ConstructRule
+		cleanWebhook.Channel = webhook_.Channel
+	}
+	err = cleanWebhook.Update()
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	c.JSON(http.StatusOK, gin.H{
+		"success": true,
+		"message": "",
+		"data":    cleanWebhook,
+	})
+	return
+}
+
+func TriggerWebhook(c *gin.Context) {
+	var reqText string
+	err := c.Bind(&reqText)
+	if err != nil {
+		c.JSON(http.StatusBadRequest, gin.H{
+			"success": false,
+			"message": err.Error(),
+		})
+		return
+	}
+	link := c.Param("link")
+	webhook, err := model.GetWebhookByLink(link)
+	if err != nil {
+		c.JSON(http.StatusNotFound, gin.H{
+			"success": false,
+			"message": "Webhook 不存在",
+		})
+		return
+	}
+	if webhook.Status != common.WebhookStatusEnabled {
+		c.JSON(http.StatusForbidden, gin.H{
+			"success": false,
+			"message": "Webhook 未启用",
+		})
+		return
+	}
+	user, err := model.GetUserById(webhook.UserId, false)
+	if err != nil {
+		c.JSON(http.StatusNotFound, gin.H{
+			"success": false,
+			"message": "用户不存在",
+		})
+		return
+	}
+	if user.Status != common.UserStatusEnabled {
+		c.JSON(http.StatusForbidden, gin.H{
+			"success": false,
+			"message": "用户已被封禁",
+		})
+		return
+	}
+	extractRule := make(map[string]string)
+	err = json.Unmarshal([]byte(webhook.ExtractRule), &extractRule)
+	if err != nil {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"success": false,
+			"message": "Webhook 提取规则解析失败",
+		})
+		return
+	}
+	for key, value := range extractRule {
+		variableValue := gjson.Get(reqText, value).String()
+		webhook.ConstructRule = strings.Replace(webhook.ConstructRule, "$"+key, variableValue, -1)
+	}
+	constructRule := model.WebhookConstructRule{}
+	err = json.Unmarshal([]byte(webhook.ConstructRule), &constructRule)
+	if err != nil {
+		c.JSON(http.StatusOK, gin.H{
+			"success": false,
+			"message": "Webhook 构建规则解析失败",
+		})
+		return
+	}
+	message := &model.Message{
+		Channel:     webhook.Channel,
+		Title:       constructRule.Title,
+		Description: constructRule.Description,
+		Content:     constructRule.Content,
+		URL:         constructRule.URL,
+	}
+	processMessage(c, message, user)
+}

+ 3 - 0
go.mod

@@ -45,6 +45,9 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/pelletier/go-toml/v2 v2.0.6 // indirect
+	github.com/tidwall/gjson v1.14.4 // indirect
+	github.com/tidwall/match v1.1.1 // indirect
+	github.com/tidwall/pretty v1.2.1 // indirect
 	github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
 	github.com/ugorji/go/codec v1.2.9 // indirect
 	golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect

+ 7 - 0
go.sum

@@ -122,6 +122,13 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
+github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
+github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
+github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
+github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
+github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
+github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
 github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
 github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
 github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=

+ 4 - 0
model/main.go

@@ -68,6 +68,10 @@ func InitDB() (err error) {
 		if err != nil {
 			return err
 		}
+		err = db.AutoMigrate(&Webhook{})
+		if err != nil {
+			return err
+		}
 		err = createRootAccountIfNeed()
 		return err
 	} else {

+ 89 - 0
model/webhook.go

@@ -0,0 +1,89 @@
+package model
+
+import (
+	"errors"
+)
+
+// WebhookConstructRule Keep compatible with Message
+type WebhookConstructRule struct {
+	Title       string `json:"title"`
+	Description string `json:"description"`
+	Content     string `json:"content"`
+	URL         string `json:"url"`
+}
+
+type Webhook struct {
+	Id            int    `json:"id"`
+	UserId        int    `json:"user_id" gorm:"index"`
+	Name          string `json:"name" gorm:"type:varchar(32);index"`
+	Status        int    `json:"status" gorm:"default:1"` // enabled, disabled
+	Link          string `json:"link" gorm:"type:char(32);uniqueIndex"`
+	CreatedTime   int64  `json:"created_time" gorm:"bigint"`
+	ExtractRule   string `json:"extract_rule" gorm:"not null"`              // how we extract key info from the request
+	ConstructRule string `json:"construct_rule" gorm:"not null"`            // how we construct message with the extracted info
+	Channel       string `json:"channel" gorm:"type:varchar(32); not null"` // which channel to send our message
+}
+
+func GetWebhookById(id int, userId int) (*Webhook, error) {
+	if id == 0 || userId == 0 {
+		return nil, errors.New("id 或 userId 为空!")
+	}
+	c := Webhook{Id: id, UserId: userId}
+	err := DB.Where(c).First(&c).Error
+	return &c, err
+}
+
+func GetWebhookByLink(link string) (*Webhook, error) {
+	if link == "" {
+		return nil, errors.New("link 为空!")
+	}
+	c := Webhook{Link: link}
+	err := DB.Where(c).First(&c).Error
+	return &c, err
+}
+
+func GetWebhooksByUserId(userId int, startIdx int, num int) (webhooks []*Webhook, err error) {
+	err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&webhooks).Error
+	return webhooks, err
+}
+
+func SearchWebhooks(userId int, keyword string) (webhooks []*Webhook, err error) {
+	err = DB.Where("user_id = ?", userId).Where("id = ? or name LIKE ?", keyword, keyword+"%").Find(&webhooks).Error
+	return webhooks, err
+}
+
+func DeleteWebhookById(id int, userId int) (c *Webhook, err error) {
+	// Why we need userId here? In case user want to delete other's c.
+	if id == 0 || userId == 0 {
+		return nil, errors.New("id 或 userId 为空!")
+	}
+	c = &Webhook{Id: id, UserId: userId}
+	err = DB.Where(c).First(&c).Error
+	if err != nil {
+		return nil, err
+	}
+	return c, c.Delete()
+}
+
+func (webhook *Webhook) Insert() error {
+	var err error
+	err = DB.Create(webhook).Error
+	return err
+}
+
+func (webhook *Webhook) UpdateStatus(status int) error {
+	err := DB.Model(webhook).Update("status", status).Error
+	return err
+}
+
+// Update Make sure your token's fields is completed, because this will update non-zero values
+func (webhook *Webhook) Update() error {
+	var err error
+	err = DB.Model(webhook).Select("name", "extract_rule", "construct_rule", "channel").Updates(webhook).Error
+	return err
+}
+
+func (webhook *Webhook) Delete() error {
+	err := DB.Delete(webhook).Error
+	return err
+}

+ 15 - 0
router/api-router.go

@@ -75,6 +75,16 @@ func SetApiRouter(router *gin.Engine) {
 			channelRoute.PUT("/", controller.UpdateChannel)
 			channelRoute.DELETE("/:id", controller.DeleteChannel)
 		}
+		webhookRoute := apiRouter.Group("/webhook")
+		webhookRoute.Use(middleware.UserAuth())
+		{
+			webhookRoute.GET("/", controller.GetAllWebhooks)
+			webhookRoute.GET("/search", controller.SearchWebhooks)
+			webhookRoute.GET("/:id", controller.GetWebhook)
+			webhookRoute.POST("/", controller.AddWebhook)
+			webhookRoute.PUT("/", controller.UpdateWebhook)
+			webhookRoute.DELETE("/:id", controller.DeleteWebhook)
+		}
 	}
 	pushRouter := router.Group("/push")
 	pushRouter.Use(middleware.GlobalAPIRateLimit())
@@ -82,4 +92,9 @@ func SetApiRouter(router *gin.Engine) {
 		pushRouter.GET("/:username", controller.GetPushMessage)
 		pushRouter.POST("/:username", controller.PostPushMessage)
 	}
+	webhookRouter := router.Group("/webhook")
+	webhookRouter.Use(middleware.GlobalAPIRateLimit())
+	{
+		webhookRouter.POST("/:link", controller.TriggerWebhook)
+	}
 }