feat: webhook server part done (#76)
This commit is contained in:
@@ -113,3 +113,9 @@ const (
|
|||||||
ChannelStatusEnabled = 1
|
ChannelStatusEnabled = 1
|
||||||
ChannelStatusDisabled = 2
|
ChannelStatusDisabled = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
WebhookStatusUnknown = 0
|
||||||
|
WebhookStatusEnabled = 1
|
||||||
|
WebhookStatusDisabled = 2
|
||||||
|
)
|
||||||
|
|||||||
@@ -116,6 +116,10 @@ func pushMessageHelper(c *gin.Context, message *model.Message) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
processMessage(c, message, &user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func processMessage(c *gin.Context, message *model.Message, user *model.User) {
|
||||||
if message.Title == "" {
|
if message.Title == "" {
|
||||||
message.Title = common.SystemName
|
message.Title = common.SystemName
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,7 @@ func pushMessageHelper(c *gin.Context, message *model.Message) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = saveAndSendMessage(&user, message, channel_)
|
err = saveAndSendMessage(user, message, channel_)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -45,6 +45,9 @@ require (
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.0.6 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.9 // indirect
|
github.com/ugorji/go/codec v1.2.9 // indirect
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
|
|||||||
@@ -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.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
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=
|
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
|
||||||
|
|||||||
@@ -68,6 +68,10 @@ func InitDB() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = db.AutoMigrate(&Webhook{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
err = createRootAccountIfNeed()
|
err = createRootAccountIfNeed()
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -75,6 +75,16 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
channelRoute.PUT("/", controller.UpdateChannel)
|
channelRoute.PUT("/", controller.UpdateChannel)
|
||||||
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
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 := router.Group("/push")
|
||||||
pushRouter.Use(middleware.GlobalAPIRateLimit())
|
pushRouter.Use(middleware.GlobalAPIRateLimit())
|
||||||
@@ -82,4 +92,9 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
pushRouter.GET("/:username", controller.GetPushMessage)
|
pushRouter.GET("/:username", controller.GetPushMessage)
|
||||||
pushRouter.POST("/:username", controller.PostPushMessage)
|
pushRouter.POST("/:username", controller.PostPushMessage)
|
||||||
}
|
}
|
||||||
|
webhookRouter := router.Group("/webhook")
|
||||||
|
webhookRouter.Use(middleware.GlobalAPIRateLimit())
|
||||||
|
{
|
||||||
|
webhookRouter.POST("/:link", controller.TriggerWebhook)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user