feat: webhook server part done (#76)

This commit is contained in:
JustSong
2023-05-10 11:58:55 +08:00
parent f7999d944e
commit 374400479f
8 changed files with 385 additions and 1 deletions

View File

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

View File

@@ -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
controller/webhook.go Normal file
View File

@@ -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
go.mod
View File

@@ -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
go.sum
View File

@@ -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=

View File

@@ -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
model/webhook.go Normal file
View File

@@ -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
}

View File

@@ -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)
}
}