feat: channel email is done

This commit is contained in:
JustSong
2022-11-11 17:24:03 +08:00
parent f1d5d9c8d1
commit e1d09aa58f
15 changed files with 196 additions and 67 deletions

1
channel/ding-talk.go Normal file
View File

@@ -0,0 +1 @@
package channel

18
channel/email.go Normal file
View File

@@ -0,0 +1,18 @@
package channel
import (
"errors"
"message-pusher/common"
"message-pusher/model"
)
func SendEmailMessage(message *Message, user *model.User) error {
if user.Email == "" {
return errors.New("未配置邮箱地址")
}
subject := message.Description
if subject == "" {
subject = message.Title
}
return common.SendEmail(subject, user.Email, message.Content)
}

1
channel/lark.go Normal file
View File

@@ -0,0 +1 @@
package channel

39
channel/main.go Normal file
View File

@@ -0,0 +1,39 @@
package channel
import (
"errors"
"message-pusher/model"
)
const (
TypeEmail = "email"
TypeWeChatTestAccount = "test"
TypeWeChatCorpAccount = "corp"
TypeLark = "lark"
TypeDingTalk = "ding"
TypeTelegram = "telegram"
)
type Message struct {
Title string `json:"title"`
Description string `json:"description"`
Content string `json:"content"`
URL string `json:"url"`
Channel string `json:"channel"`
Token string `json:"token"`
}
func (message *Message) Send(user *model.User) error {
switch message.Channel {
case TypeEmail:
return SendEmailMessage(message, user)
case TypeWeChatTestAccount:
case TypeWeChatCorpAccount:
case TypeLark:
case TypeDingTalk:
case TypeTelegram:
default:
return errors.New("不支持的消息通道:" + message.Channel)
}
return nil
}

1
channel/telegram.go Normal file
View File

@@ -0,0 +1 @@
package channel

View File

@@ -0,0 +1 @@
package channel

View File

@@ -0,0 +1 @@
package channel

View File

@@ -75,6 +75,7 @@ var (
var RateLimitKeyExpirationDuration = 20 * time.Minute var RateLimitKeyExpirationDuration = 20 * time.Minute
const ( const (
UserStatusEnabled = 1 // don't use 0, 0 is the default value! UserStatusNonExisted = 0
UserStatusDisabled = 2 // also don't use 0 UserStatusEnabled = 1 // don't use 0, 0 is the default value!
UserStatusDisabled = 2 // also don't use 0
) )

View File

@@ -1,6 +1,8 @@
package common package common
import "gopkg.in/gomail.v2" import (
"gopkg.in/gomail.v2"
)
func SendEmail(subject string, receiver string, content string) error { func SendEmail(subject string, receiver string, content string) error {
m := gomail.NewMessage() m := gomail.NewMessage()

109
controller/message.go Normal file
View File

@@ -0,0 +1,109 @@
package controller
import (
"bytes"
"encoding/json"
"github.com/gin-gonic/gin"
"github.com/yuin/goldmark"
"message-pusher/channel"
"message-pusher/common"
"message-pusher/model"
"net/http"
)
func GetPushMessage(c *gin.Context) {
message := channel.Message{
Title: c.Query("title"),
Description: c.Query("description"),
Content: c.Query("content"),
URL: c.Query("url"),
Channel: c.Query("channel"),
Token: c.Query("token"),
}
if message.Description == "" {
// Keep compatible with ServerChan
message.Description = c.Query("desp")
}
pushMessageHelper(c, &message)
}
func PostPushMessage(c *gin.Context) {
message := channel.Message{}
err := json.NewDecoder(c.Request.Body).Decode(&message)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无法解析请求体,请检查其是否为合法 JSON",
})
return
}
pushMessageHelper(c, &message)
}
func pushMessageHelper(c *gin.Context, message *channel.Message) {
user := model.User{Username: c.Param("username")}
user.FillUserByUsername()
if user.Status == common.UserStatusNonExisted {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "用户不存在",
})
return
}
if user.Status == common.UserStatusDisabled {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "用户已被封禁",
})
return
}
if user.Token != "" {
if message.Token == "" {
message.Token = c.Request.Header.Get("Authorization")
}
if user.Token != message.Token {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"message": "无效的 token",
})
return
}
}
if message.Title == "" {
message.Title = common.SystemName
}
if message.Content != "" {
var buf bytes.Buffer
err := goldmark.Convert([]byte(message.Content), &buf)
if err != nil {
common.SysLog(err.Error())
} else {
message.Content = buf.String()
}
} else {
if message.Description != "" {
message.Content = message.Description
} else {
message.Content = "无内容"
}
}
if message.Channel == "" {
message.Channel = user.Channel
if message.Channel == "" {
message.Channel = channel.TypeEmail
}
}
err := message.Send(&user)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "ok",
})
return
}

1
go.mod
View File

@@ -41,6 +41,7 @@ require (
github.com/pelletier/go-toml/v2 v2.0.1 // indirect github.com/pelletier/go-toml/v2 v2.0.1 // indirect
github.com/stretchr/testify v1.8.0 // indirect github.com/stretchr/testify v1.8.0 // indirect
github.com/ugorji/go/codec v1.2.7 // indirect github.com/ugorji/go/codec v1.2.7 // indirect
github.com/yuin/goldmark v1.5.2 // indirect
golang.org/x/net v0.1.0 // indirect golang.org/x/net v0.1.0 // indirect
golang.org/x/sys v0.1.0 // indirect golang.org/x/sys v0.1.0 // indirect
golang.org/x/text v0.4.0 // indirect golang.org/x/text v0.4.0 // indirect

2
go.sum
View File

@@ -94,6 +94,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/yuin/goldmark v1.5.2 h1:ALmeCk/px5FSm1MAcFBAsVKZjDuMVj8Tm7FFIlMJnqU=
github.com/yuin/goldmark v1.5.2/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU=
golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw=

View File

@@ -4,7 +4,6 @@ import (
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"message-pusher/common" "message-pusher/common"
"message-pusher/model"
"net/http" "net/http"
) )
@@ -14,34 +13,13 @@ func authHelper(c *gin.Context, minRole int) {
role := session.Get("role") role := session.Get("role")
id := session.Get("id") id := session.Get("id")
status := session.Get("status") status := session.Get("status")
authByToken := false
if username == nil { if username == nil {
// Check token c.JSON(http.StatusOK, gin.H{
token := c.Request.Header.Get("Authorization") "success": false,
if token == "" { "message": "无权进行此操作,未登录或 token 无效",
c.JSON(http.StatusOK, gin.H{ })
"success": false, c.Abort()
"message": "无权进行此操作,未登录或 token 无效", return
})
c.Abort()
return
}
user := model.ValidateUserToken(token)
if user != nil && user.Username != "" {
// Token is valid
username = user.Username
role = user.Role
id = user.Id
status = user.Status
} else {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "无权进行此操作,未登录或 token 无效",
})
c.Abort()
return
}
authByToken = true
} }
if status.(int) == common.UserStatusDisabled { if status.(int) == common.UserStatusDisabled {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
@@ -62,7 +40,6 @@ func authHelper(c *gin.Context, minRole int) {
c.Set("username", username) c.Set("username", username)
c.Set("role", role) c.Set("role", role)
c.Set("id", id) c.Set("id", id)
c.Set("authByToken", authByToken)
c.Next() c.Next()
} }
@@ -83,35 +60,3 @@ func RootAuth() func(c *gin.Context) {
authHelper(c, common.RoleRootUser) authHelper(c, common.RoleRootUser)
} }
} }
// NoTokenAuth You should always use this after normal auth middlewares.
func NoTokenAuth() func(c *gin.Context) {
return func(c *gin.Context) {
authByToken := c.GetBool("authByToken")
if authByToken {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "本接口不支持使用 token 进行验证",
})
c.Abort()
return
}
c.Next()
}
}
// TokenOnlyAuth You should always use this after normal auth middlewares.
func TokenOnlyAuth() func(c *gin.Context) {
return func(c *gin.Context) {
authByToken := c.GetBool("authByToken")
if !authByToken {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "本接口仅支持使用 token 进行验证",
})
c.Abort()
return
}
c.Next()
}
}

View File

@@ -17,6 +17,7 @@ type User struct {
Email string `json:"email" gorm:"index" validate:"max=50"` Email string `json:"email" gorm:"index" validate:"max=50"`
GitHubId string `json:"github_id" gorm:"column:github_id;index"` GitHubId string `json:"github_id" gorm:"column:github_id;index"`
WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"`
Channel string `json:"channel"`
VerificationCode string `json:"verification_code" gorm:"-:all"` VerificationCode string `json:"verification_code" gorm:"-:all"`
} }

View File

@@ -28,7 +28,7 @@ func SetApiRouter(router *gin.Engine) {
userRoute.GET("/logout", controller.Logout) userRoute.GET("/logout", controller.Logout)
selfRoute := userRoute.Group("/") selfRoute := userRoute.Group("/")
selfRoute.Use(middleware.UserAuth(), middleware.NoTokenAuth()) selfRoute.Use(middleware.UserAuth())
{ {
selfRoute.GET("/self", controller.GetSelf) selfRoute.GET("/self", controller.GetSelf)
selfRoute.PUT("/self", controller.UpdateSelf) selfRoute.PUT("/self", controller.UpdateSelf)
@@ -37,7 +37,7 @@ func SetApiRouter(router *gin.Engine) {
} }
adminRoute := userRoute.Group("/") adminRoute := userRoute.Group("/")
adminRoute.Use(middleware.AdminAuth(), middleware.NoTokenAuth()) adminRoute.Use(middleware.AdminAuth())
{ {
adminRoute.GET("/", controller.GetAllUsers) adminRoute.GET("/", controller.GetAllUsers)
adminRoute.GET("/search", controller.SearchUsers) adminRoute.GET("/search", controller.SearchUsers)
@@ -49,7 +49,7 @@ func SetApiRouter(router *gin.Engine) {
} }
} }
optionRoute := apiRouter.Group("/option") optionRoute := apiRouter.Group("/option")
optionRoute.Use(middleware.RootAuth(), middleware.NoTokenAuth()) optionRoute.Use(middleware.RootAuth())
{ {
optionRoute.GET("/", controller.GetOptions) optionRoute.GET("/", controller.GetOptions)
optionRoute.PUT("/", controller.UpdateOption) optionRoute.PUT("/", controller.UpdateOption)
@@ -61,4 +61,10 @@ func SetApiRouter(router *gin.Engine) {
fileRoute.DELETE("/:id", middleware.UserAuth(), controller.DeleteFile) fileRoute.DELETE("/:id", middleware.UserAuth(), controller.DeleteFile)
} }
} }
pushRouter := router.Group("/push")
pushRouter.Use(middleware.GlobalAPIRateLimit())
{
pushRouter.GET("/:username", controller.GetPushMessage)
pushRouter.POST("/:username", controller.PostPushMessage)
}
} }