feat: add support for client channel
This commit is contained in:
@@ -0,0 +1,82 @@
|
|||||||
|
package channel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"message-pusher/common"
|
||||||
|
"message-pusher/model"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clientConnMap map[int]*websocket.Conn
|
||||||
|
var clientConnMapMutex sync.Mutex
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
clientConnMapMutex.Lock()
|
||||||
|
clientConnMap = make(map[int]*websocket.Conn)
|
||||||
|
clientConnMapMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendMessageWithConn(message *Message, conn *websocket.Conn) error {
|
||||||
|
return conn.WriteJSON(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LogoutClient(userId int) {
|
||||||
|
clientConnMapMutex.Lock()
|
||||||
|
delete(clientConnMap, userId)
|
||||||
|
clientConnMapMutex.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterClient(userId int, conn *websocket.Conn) {
|
||||||
|
clientConnMapMutex.Lock()
|
||||||
|
oldConn, existed := clientConnMap[userId]
|
||||||
|
clientConnMapMutex.Unlock()
|
||||||
|
if existed {
|
||||||
|
byeMessage := &Message{
|
||||||
|
Title: common.SystemName,
|
||||||
|
Description: "其他客户端已连接服务器,本客户端已被挤下线!",
|
||||||
|
}
|
||||||
|
err := SendMessageWithConn(byeMessage, oldConn)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error send message to client: " + err.Error())
|
||||||
|
}
|
||||||
|
err = oldConn.Close()
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error close WebSocket connection: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
helloMessage := &Message{
|
||||||
|
Title: common.SystemName,
|
||||||
|
Description: "客户端连接成功!",
|
||||||
|
}
|
||||||
|
err := SendMessageWithConn(helloMessage, conn)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("error send message to client: " + err.Error())
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
clientConnMapMutex.Lock()
|
||||||
|
clientConnMap[userId] = conn
|
||||||
|
clientConnMapMutex.Unlock()
|
||||||
|
conn.SetCloseHandler(func(code int, text string) error {
|
||||||
|
LogoutClient(userId)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendClientMessage(message *Message, user *model.User) error {
|
||||||
|
if user.ClientSecret == "" {
|
||||||
|
return errors.New("未配置 WebSocket 客户端消息推送方式")
|
||||||
|
}
|
||||||
|
clientConnMapMutex.Lock()
|
||||||
|
conn, existed := clientConnMap[user.Id]
|
||||||
|
clientConnMapMutex.Unlock()
|
||||||
|
if !existed {
|
||||||
|
return errors.New("客户端未连接")
|
||||||
|
}
|
||||||
|
err := SendMessageWithConn(message, conn)
|
||||||
|
if err != nil {
|
||||||
|
LogoutClient(user.Id)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ const (
|
|||||||
TypeDing = "ding"
|
TypeDing = "ding"
|
||||||
TypeTelegram = "telegram"
|
TypeTelegram = "telegram"
|
||||||
TypeBark = "bark"
|
TypeBark = "bark"
|
||||||
|
TypeClient = "client"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
@@ -42,6 +43,8 @@ func (message *Message) Send(user *model.User) error {
|
|||||||
return SendDingMessage(message, user)
|
return SendDingMessage(message, user)
|
||||||
case TypeBark:
|
case TypeBark:
|
||||||
return SendBarkMessage(message, user)
|
return SendBarkMessage(message, user)
|
||||||
|
case TypeClient:
|
||||||
|
return SendClientMessage(message, user)
|
||||||
default:
|
default:
|
||||||
return errors.New("不支持的消息通道:" + message.Channel)
|
return errors.New("不支持的消息通道:" + message.Channel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ func UpdateSelf(c *gin.Context) {
|
|||||||
DingWebhookSecret: user.DingWebhookSecret,
|
DingWebhookSecret: user.DingWebhookSecret,
|
||||||
BarkServer: user.BarkServer,
|
BarkServer: user.BarkServer,
|
||||||
BarkSecret: user.BarkSecret,
|
BarkSecret: user.BarkSecret,
|
||||||
|
ClientSecret: user.ClientSecret,
|
||||||
}
|
}
|
||||||
channel.TokenStoreUpdateUser(&cleanUser, originUser)
|
channel.TokenStoreUpdateUser(&cleanUser, originUser)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"message-pusher/channel"
|
||||||
|
"message-pusher/model"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{} // use default options
|
||||||
|
|
||||||
|
func RegisterClient(c *gin.Context) {
|
||||||
|
secret := c.Query("secret")
|
||||||
|
if secret == "" {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "secret 为空",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
user := model.User{Username: c.Param("username")}
|
||||||
|
err := user.FillUserByUsername()
|
||||||
|
if secret != user.ClientSecret {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": "用户名与密钥不匹配",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channel.RegisterClient(user.Id, conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -32,6 +32,7 @@ require (
|
|||||||
github.com/gorilla/context v1.1.1 // indirect
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/securecookie v1.1.1 // indirect
|
github.com/gorilla/securecookie v1.1.1 // indirect
|
||||||
github.com/gorilla/sessions v1.2.1 // indirect
|
github.com/gorilla/sessions v1.2.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.5.0 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
github.com/json-iterator/go v1.1.12 // indirect
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
|||||||
@@ -54,6 +54,8 @@ github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+
|
|||||||
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
github.com/gorilla/sessions v1.1.1/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
||||||
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI=
|
||||||
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
|
||||||
|
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||||
|
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ type User struct {
|
|||||||
DingWebhookSecret string `json:"ding_webhook_secret"`
|
DingWebhookSecret string `json:"ding_webhook_secret"`
|
||||||
BarkServer string `json:"bark_server"`
|
BarkServer string `json:"bark_server"`
|
||||||
BarkSecret string `json:"bark_secret"`
|
BarkSecret string `json:"bark_secret"`
|
||||||
|
ClientSecret string `json:"client_secret"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMaxUserId() int {
|
func GetMaxUserId() int {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
apiRouter.GET("/about", controller.GetAbout)
|
apiRouter.GET("/about", controller.GetAbout)
|
||||||
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
apiRouter.GET("/verification", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendEmailVerification)
|
||||||
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail)
|
||||||
|
apiRouter.GET("/register_client/:username", middleware.CriticalRateLimit(), controller.RegisterClient)
|
||||||
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword)
|
||||||
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth)
|
||||||
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const PushSetting = () => {
|
|||||||
ding_webhook_secret: '',
|
ding_webhook_secret: '',
|
||||||
bark_server: '',
|
bark_server: '',
|
||||||
bark_secret: '',
|
bark_secret: '',
|
||||||
|
client_secret: '',
|
||||||
});
|
});
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
@@ -106,6 +107,9 @@ const PushSetting = () => {
|
|||||||
data.bark_server = removeTrailingSlash(inputs.bark_server);
|
data.bark_server = removeTrailingSlash(inputs.bark_server);
|
||||||
data.bark_secret = inputs.bark_secret;
|
data.bark_secret = inputs.bark_secret;
|
||||||
break;
|
break;
|
||||||
|
case 'client':
|
||||||
|
data.client_secret = inputs.client_secret;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
showError(`无效的参数:${which}`);
|
showError(`无效的参数:${which}`);
|
||||||
return;
|
return;
|
||||||
@@ -151,6 +155,7 @@ const PushSetting = () => {
|
|||||||
{ key: 'lark', text: '飞书群机器人', value: 'lark' },
|
{ key: 'lark', text: '飞书群机器人', value: 'lark' },
|
||||||
{ key: 'ding', text: '钉钉群机器人', value: 'ding' },
|
{ key: 'ding', text: '钉钉群机器人', value: 'ding' },
|
||||||
{ key: 'bark', text: 'Bark App', value: 'bark' },
|
{ key: 'bark', text: 'Bark App', value: 'bark' },
|
||||||
|
{ key: 'client', text: 'WebSocket 客户端', value: 'client' },
|
||||||
]}
|
]}
|
||||||
value={inputs.channel}
|
value={inputs.channel}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
@@ -443,6 +448,36 @@ const PushSetting = () => {
|
|||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => test('bark')}>测试</Button>
|
<Button onClick={() => test('bark')}>测试</Button>
|
||||||
|
<Divider />
|
||||||
|
<Header as='h3'>
|
||||||
|
WebSocket 客户端设置(client)
|
||||||
|
<Header.Subheader>
|
||||||
|
通过 WebSocket
|
||||||
|
客户端进行推送,可以使用官方客户端实现,或者根据协议自行实现。官方客户端
|
||||||
|
<a
|
||||||
|
target='_blank'
|
||||||
|
href='https://github.com/songquanpeng/personal-assistant'
|
||||||
|
>
|
||||||
|
详见此处
|
||||||
|
</a>
|
||||||
|
。
|
||||||
|
</Header.Subheader>
|
||||||
|
</Header>
|
||||||
|
<Form.Group widths={2}>
|
||||||
|
<Form.Input
|
||||||
|
label='服务器连接密钥'
|
||||||
|
name='client_secret'
|
||||||
|
type='password'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
autoComplete='off'
|
||||||
|
value={inputs.client_secret}
|
||||||
|
placeholder='在此设置服务器连接密钥'
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Button onClick={() => submit('client')} loading={loading}>
|
||||||
|
保存
|
||||||
|
</Button>
|
||||||
|
<Button onClick={() => test('client')}>测试</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</Grid.Column>
|
</Grid.Column>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
Reference in New Issue
Block a user