From 3a8d625201adc606210af4924b8885f41571d9a3 Mon Sep 17 00:00:00 2001 From: JustSong Date: Fri, 5 May 2023 10:43:56 +0800 Subject: [PATCH 1/3] feat: now server part supports multiple channels for the same type (#50) --- channel/bark.go | 7 +- channel/client.go | 28 ++-- channel/corp.go | 7 +- channel/ding.go | 9 +- channel/discord.go | 7 +- channel/email.go | 2 +- channel/lark.go | 9 +- channel/main.go | 62 ++++----- channel/telegram.go | 9 +- channel/token-store.go | 245 +++++++++++++++++++-------------- channel/wechat-corp-account.go | 32 +++-- channel/wechat-test-account.go | 15 +- common/constants.go | 6 + controller/channel.go | 183 ++++++++++++++++++++++++ controller/message.go | 32 +++-- controller/user.go | 42 +----- controller/websocket.go | 25 +++- model/channel.go | 106 ++++++++++++++ model/main.go | 4 + model/user.go | 59 ++------ router/api-router.go | 10 ++ 21 files changed, 601 insertions(+), 298 deletions(-) create mode 100644 controller/channel.go create mode 100644 model/channel.go diff --git a/channel/bark.go b/channel/bark.go index 4795706..5e8924d 100644 --- a/channel/bark.go +++ b/channel/bark.go @@ -20,11 +20,8 @@ type barkMessageResponse struct { Message string `json:"message"` } -func SendBarkMessage(message *model.Message, user *model.User) error { - if user.BarkServer == "" || user.BarkSecret == "" { - return errors.New("未配置 Bark 消息推送方式") - } - url := fmt.Sprintf("%s/%s", user.BarkServer, user.BarkSecret) +func SendBarkMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { + url := fmt.Sprintf("%s/%s", channel_.URL, channel_.Secret) req := barkMessageRequest{ Title: message.Title, Body: message.Content, diff --git a/channel/client.go b/channel/client.go index b59b576..c911e28 100644 --- a/channel/client.go +++ b/channel/client.go @@ -2,6 +2,7 @@ package channel import ( "errors" + "fmt" "github.com/gorilla/websocket" "message-pusher/common" "message-pusher/model" @@ -17,7 +18,7 @@ const ( ) type webSocketClient struct { - userId int + key string conn *websocket.Conn message chan *model.Message pong chan bool @@ -55,10 +56,10 @@ func (c *webSocketClient) handleDataWriting() { defer func() { pingTicker.Stop() clientConnMapMutex.Lock() - client, ok := clientMap[c.userId] + client, ok := clientMap[c.key] // otherwise we may delete the new added client! if ok && client.timestamp == c.timestamp { - delete(clientMap, c.userId) + delete(clientMap, c.key) } clientConnMapMutex.Unlock() err := c.conn.Close() @@ -108,18 +109,19 @@ func (c *webSocketClient) close() { // the defer function in handleDataWriting will do the cleanup } -var clientMap map[int]*webSocketClient +var clientMap map[string]*webSocketClient var clientConnMapMutex sync.Mutex func init() { clientConnMapMutex.Lock() - clientMap = make(map[int]*webSocketClient) + clientMap = make(map[string]*webSocketClient) clientConnMapMutex.Unlock() } -func RegisterClient(userId int, conn *websocket.Conn) { +func RegisterClient(channelName string, userId int, conn *websocket.Conn) { + key := fmt.Sprintf("%s:%d", channelName, userId) clientConnMapMutex.Lock() - oldClient, existed := clientMap[userId] + oldClient, existed := clientMap[key] clientConnMapMutex.Unlock() if existed { byeMessage := &model.Message{ @@ -134,7 +136,7 @@ func RegisterClient(userId int, conn *websocket.Conn) { Description: "客户端连接成功!", } newClient := &webSocketClient{ - userId: userId, + key: key, conn: conn, message: make(chan *model.Message), pong: make(chan bool), @@ -145,16 +147,14 @@ func RegisterClient(userId int, conn *websocket.Conn) { go newClient.handleDataReading() defer newClient.sendMessage(helloMessage) clientConnMapMutex.Lock() - clientMap[userId] = newClient + clientMap[key] = newClient clientConnMapMutex.Unlock() } -func SendClientMessage(message *model.Message, user *model.User) error { - if user.ClientSecret == "" { - return errors.New("未配置 WebSocket 客户端消息推送方式") - } +func SendClientMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { + key := fmt.Sprintf("%s:%d", channel_.Name, user.Id) clientConnMapMutex.Lock() - client, existed := clientMap[user.Id] + client, existed := clientMap[key] clientConnMapMutex.Unlock() if !existed { return errors.New("客户端未连接") diff --git a/channel/corp.go b/channel/corp.go index 9329a07..67d80c4 100644 --- a/channel/corp.go +++ b/channel/corp.go @@ -26,11 +26,8 @@ type corpMessageResponse struct { Message string `json:"errmsg"` } -func SendCorpMessage(message *model.Message, user *model.User) error { +func SendCorpMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { // https://developer.work.weixin.qq.com/document/path/91770 - if user.CorpWebhookURL == "" { - return errors.New("未配置企业微信群机器人消息推送方式") - } messageRequest := corpMessageRequest{ MessageType: "text", } @@ -48,7 +45,7 @@ func SendCorpMessage(message *model.Message, user *model.User) error { if err != nil { return err } - resp, err := http.Post(fmt.Sprintf("%s", user.CorpWebhookURL), "application/json", + resp, err := http.Post(fmt.Sprintf("%s", channel_.URL), "application/json", bytes.NewBuffer(jsonData)) if err != nil { return err diff --git a/channel/ding.go b/channel/ding.go index 3b7df8d..3390752 100644 --- a/channel/ding.go +++ b/channel/ding.go @@ -35,11 +35,8 @@ type dingMessageResponse struct { Message string `json:"errmsg"` } -func SendDingMessage(message *model.Message, user *model.User) error { +func SendDingMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { // https://open.dingtalk.com/document/robots/custom-robot-access#title-72m-8ag-pqw - if user.DingWebhookURL == "" { - return errors.New("未配置钉钉群机器人消息推送方式") - } messageRequest := dingMessageRequest{ MessageType: "text", } @@ -60,7 +57,7 @@ func SendDingMessage(message *model.Message, user *model.User) error { } timestamp := time.Now().UnixMilli() - sign, err := dingSign(user.DingWebhookSecret, timestamp) + sign, err := dingSign(channel_.Secret, timestamp) if err != nil { return err } @@ -68,7 +65,7 @@ func SendDingMessage(message *model.Message, user *model.User) error { if err != nil { return err } - resp, err := http.Post(fmt.Sprintf("%s×tamp=%d&sign=%s", user.DingWebhookURL, timestamp, sign), "application/json", + resp, err := http.Post(fmt.Sprintf("%s×tamp=%d&sign=%s", channel_.URL, timestamp, sign), "application/json", bytes.NewBuffer(jsonData)) if err != nil { return err diff --git a/channel/discord.go b/channel/discord.go index 60ad019..7e87106 100644 --- a/channel/discord.go +++ b/channel/discord.go @@ -18,10 +18,7 @@ type discordMessageResponse struct { Message string `json:"message"` } -func SendDiscordMessage(message *model.Message, user *model.User) error { - if user.DiscordWebhookURL == "" { - return errors.New("未配置 Discord 群机器人消息推送方式") - } +func SendDiscordMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { if message.Content == "" { message.Content = message.Description } @@ -42,7 +39,7 @@ func SendDiscordMessage(message *model.Message, user *model.User) error { if err != nil { return err } - resp, err := http.Post(user.DiscordWebhookURL, "application/json", bytes.NewBuffer(jsonData)) + resp, err := http.Post(channel_.URL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return err } diff --git a/channel/email.go b/channel/email.go index 63729fb..2861c06 100644 --- a/channel/email.go +++ b/channel/email.go @@ -6,7 +6,7 @@ import ( "message-pusher/model" ) -func SendEmailMessage(message *model.Message, user *model.User) error { +func SendEmailMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { if message.To != "" { if user.SendEmailToOthers != common.SendEmailToOthersAllowed && user.Role < common.RoleAdminUser { return errors.New("没有权限发送邮件给其他人,请联系管理员为你添加该权限") diff --git a/channel/lark.go b/channel/lark.go index 14b1187..5e05a72 100644 --- a/channel/lark.go +++ b/channel/lark.go @@ -46,11 +46,8 @@ type larkMessageResponse struct { Message string `json:"msg"` } -func SendLarkMessage(message *model.Message, user *model.User) error { +func SendLarkMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { // https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN#e1cdee9f - if user.LarkWebhookURL == "" { - return errors.New("未配置飞书群机器人消息推送方式") - } messageRequest := larkMessageRequest{ MessageType: "text", } @@ -83,7 +80,7 @@ func SendLarkMessage(message *model.Message, user *model.User) error { now := time.Now() timestamp := now.Unix() - sign, err := larkSign(user.LarkWebhookSecret, timestamp) + sign, err := larkSign(channel_.Secret, timestamp) if err != nil { return err } @@ -93,7 +90,7 @@ func SendLarkMessage(message *model.Message, user *model.User) error { if err != nil { return err } - resp, err := http.Post(user.LarkWebhookURL, "application/json", + resp, err := http.Post(channel_.URL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return err diff --git a/channel/main.go b/channel/main.go index c5f3327..3b79730 100644 --- a/channel/main.go +++ b/channel/main.go @@ -5,45 +5,31 @@ import ( "message-pusher/model" ) -const ( - TypeEmail = "email" - TypeWeChatTestAccount = "test" - TypeWeChatCorpAccount = "corp_app" - TypeCorp = "corp" - TypeLark = "lark" - TypeDing = "ding" - TypeTelegram = "telegram" - TypeDiscord = "discord" - TypeBark = "bark" - TypeClient = "client" - TypeNone = "none" -) - -func SendMessage(message *model.Message, user *model.User) error { - switch message.Channel { - case TypeEmail: - return SendEmailMessage(message, user) - case TypeWeChatTestAccount: - return SendWeChatTestMessage(message, user) - case TypeWeChatCorpAccount: - return SendWeChatCorpMessage(message, user) - case TypeCorp: - return SendCorpMessage(message, user) - case TypeLark: - return SendLarkMessage(message, user) - case TypeDing: - return SendDingMessage(message, user) - case TypeBark: - return SendBarkMessage(message, user) - case TypeClient: - return SendClientMessage(message, user) - case TypeTelegram: - return SendTelegramMessage(message, user) - case TypeDiscord: - return SendDiscordMessage(message, user) - case TypeNone: +func SendMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { + switch channel_.Type { + case model.TypeEmail: + return SendEmailMessage(message, user, channel_) + case model.TypeWeChatTestAccount: + return SendWeChatTestMessage(message, user, channel_) + case model.TypeWeChatCorpAccount: + return SendWeChatCorpMessage(message, user, channel_) + case model.TypeCorp: + return SendCorpMessage(message, user, channel_) + case model.TypeLark: + return SendLarkMessage(message, user, channel_) + case model.TypeDing: + return SendDingMessage(message, user, channel_) + case model.TypeBark: + return SendBarkMessage(message, user, channel_) + case model.TypeClient: + return SendClientMessage(message, user, channel_) + case model.TypeTelegram: + return SendTelegramMessage(message, user, channel_) + case model.TypeDiscord: + return SendDiscordMessage(message, user, channel_) + case model.TypeNone: return nil default: - return errors.New("不支持的消息通道:" + message.Channel) + return errors.New("不支持的消息通道:" + channel_.Type) } } diff --git a/channel/telegram.go b/channel/telegram.go index c5776ad..fab9a3b 100644 --- a/channel/telegram.go +++ b/channel/telegram.go @@ -20,13 +20,10 @@ type telegramMessageResponse struct { Description string `json:"description"` } -func SendTelegramMessage(message *model.Message, user *model.User) error { +func SendTelegramMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { // https://core.telegram.org/bots/api#sendmessage - if user.TelegramBotToken == "" || user.TelegramChatId == "" { - return errors.New("未配置 Telegram 机器人消息推送方式") - } messageRequest := telegramMessageRequest{ - ChatId: user.TelegramChatId, + ChatId: channel_.AppId, Text: message.Content, ParseMode: "markdown", } @@ -37,7 +34,7 @@ func SendTelegramMessage(message *model.Message, user *model.User) error { if err != nil { return err } - resp, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", user.TelegramBotToken), "application/json", + resp, err := http.Post(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", channel_.Secret), "application/json", bytes.NewBuffer(jsonData)) if err != nil { return err diff --git a/channel/token-store.go b/channel/token-store.go index 9a6582f..e5da0ca 100644 --- a/channel/token-store.go +++ b/channel/token-store.go @@ -12,6 +12,7 @@ type TokenStoreItem interface { Token() string Refresh() IsFilled() bool + IsShared() bool } type tokenStore struct { @@ -22,34 +23,51 @@ type tokenStore struct { var s tokenStore +func channel2item(channel_ *model.Channel) TokenStoreItem { + if channel_.Type == model.TypeWeChatTestAccount { + item := &WeChatTestAccountTokenStoreItem{ + AppID: channel_.AppId, + AppSecret: channel_.Secret, + } + return item + } else if channel_.Type == model.TypeWeChatCorpAccount { + corpId, agentId, err := parseWechatCorpAccountAppId(channel_.AppId) + if err != nil { + common.SysError(err.Error()) + return nil + } + item := &WeChatCorpAccountTokenStoreItem{ + CorpId: corpId, + AgentSecret: channel_.Secret, + AgentId: agentId, + } + return item + } + return nil +} + +func channels2items(channels []*model.Channel) []TokenStoreItem { + var items []TokenStoreItem + for _, channel_ := range channels { + item := channel2item(channel_) + if item != nil { + items = append(items, item) + } + } + return items +} + func TokenStoreInit() { s.Map = make(map[string]*TokenStoreItem) // https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html // https://developer.work.weixin.qq.com/document/path/91039 s.ExpirationSeconds = 2 * 55 * 60 // 2 hours - 5 minutes go func() { - users, err := model.GetAllUsersWithSecrets() + channels, err := model.GetTokenStoreChannels() if err != nil { common.FatalLog(err.Error()) } - var items []TokenStoreItem - for _, user := range users { - if user.WeChatTestAccountId != "" { - item := &WeChatTestAccountTokenStoreItem{ - AppID: user.WeChatTestAccountId, - AppSecret: user.WeChatTestAccountSecret, - } - items = append(items, item) - } - if user.WeChatCorpAccountId != "" { - item := &WeChatCorpAccountTokenStoreItem{ - CorpId: user.WeChatCorpAccountId, - AgentSecret: user.WeChatCorpAccountAgentSecret, - AgentId: user.WeChatCorpAccountAgentId, - } - items = append(items, item) - } - } + items := channels2items(channels) s.Mutex.RLock() for i := range items { // s.Map[item.Key()] = &item // This is wrong, you are getting the address of a local variable! @@ -99,78 +117,14 @@ func TokenStoreRemoveItem(item TokenStoreItem) { } func TokenStoreAddUser(user *model.User) { - testItem := WeChatTestAccountTokenStoreItem{ - AppID: user.WeChatTestAccountId, - AppSecret: user.WeChatTestAccountSecret, + channels, err := model.GetTokenStoreChannelsByUserId(user.Id) + if err != nil { + common.SysError(err.Error()) + return } - TokenStoreAddItem(&testItem) - corpItem := WeChatCorpAccountTokenStoreItem{ - CorpId: user.WeChatCorpAccountId, - AgentSecret: user.WeChatCorpAccountAgentSecret, - AgentId: user.WeChatCorpAccountAgentId, - } - TokenStoreAddItem(&corpItem) -} - -func TokenStoreUpdateUser(cleanUser *model.User, originUser *model.User) { - // WeChat Test Account - // The fields of cleanUser may be incomplete! - if cleanUser.WeChatTestAccountId == originUser.WeChatTestAccountId { - cleanUser.WeChatTestAccountId = "" - } - if cleanUser.WeChatTestAccountSecret == originUser.WeChatTestAccountSecret { - cleanUser.WeChatTestAccountSecret = "" - } - // This means the user updated those fields. - if cleanUser.WeChatTestAccountId != "" || cleanUser.WeChatTestAccountSecret != "" { - oldWeChatTestAccountTokenStoreItem := WeChatTestAccountTokenStoreItem{ - AppID: originUser.WeChatTestAccountId, - AppSecret: originUser.WeChatTestAccountSecret, - } - // Yeah, it's a deep copy. - newWeChatTestAccountTokenStoreItem := oldWeChatTestAccountTokenStoreItem - if cleanUser.WeChatTestAccountId != "" { - newWeChatTestAccountTokenStoreItem.AppID = cleanUser.WeChatTestAccountId - } - if cleanUser.WeChatTestAccountSecret != "" { - newWeChatTestAccountTokenStoreItem.AppSecret = cleanUser.WeChatTestAccountSecret - } - if !oldWeChatTestAccountTokenStoreItem.IsShared() { - TokenStoreRemoveItem(&oldWeChatTestAccountTokenStoreItem) - } - TokenStoreAddItem(&newWeChatTestAccountTokenStoreItem) - } - - // WeChat Corp Account - if cleanUser.WeChatCorpAccountId == originUser.WeChatCorpAccountId { - cleanUser.WeChatCorpAccountId = "" - } - if cleanUser.WeChatCorpAccountAgentId == originUser.WeChatCorpAccountAgentId { - cleanUser.WeChatCorpAccountAgentId = "" - } - if cleanUser.WeChatCorpAccountAgentSecret == originUser.WeChatCorpAccountAgentSecret { - cleanUser.WeChatCorpAccountAgentSecret = "" - } - if cleanUser.WeChatCorpAccountId != "" || cleanUser.WeChatCorpAccountAgentId != "" || cleanUser.WeChatCorpAccountAgentSecret != "" { - oldWeChatCorpAccountTokenStoreItem := WeChatCorpAccountTokenStoreItem{ - CorpId: originUser.WeChatCorpAccountId, - AgentSecret: originUser.WeChatCorpAccountAgentSecret, - AgentId: originUser.WeChatCorpAccountAgentId, - } - newWeChatCorpAccountTokenStoreItem := oldWeChatCorpAccountTokenStoreItem - if cleanUser.WeChatCorpAccountId != "" { - newWeChatCorpAccountTokenStoreItem.CorpId = cleanUser.WeChatCorpAccountId - } - if cleanUser.WeChatCorpAccountAgentSecret != "" { - newWeChatCorpAccountTokenStoreItem.AgentSecret = cleanUser.WeChatCorpAccountAgentSecret - } - if cleanUser.WeChatCorpAccountAgentId != "" { - newWeChatCorpAccountTokenStoreItem.AgentId = cleanUser.WeChatCorpAccountAgentId - } - if !oldWeChatCorpAccountTokenStoreItem.IsShared() { - TokenStoreRemoveItem(&oldWeChatCorpAccountTokenStoreItem) - } - TokenStoreAddItem(&newWeChatCorpAccountTokenStoreItem) + items := channels2items(channels) + for i := range items { + TokenStoreAddItem(items[i]) } } @@ -178,20 +132,109 @@ func TokenStoreUpdateUser(cleanUser *model.User, originUser *model.User) { // user must be filled. // It's okay to delete a user that don't have an item here. func TokenStoreRemoveUser(user *model.User) { - testAccountTokenStoreItem := WeChatTestAccountTokenStoreItem{ - AppID: user.WeChatTestAccountId, - AppSecret: user.WeChatTestAccountSecret, + channels, err := model.GetTokenStoreChannelsByUserId(user.Id) + if err != nil { + common.SysError(err.Error()) + return } - if !testAccountTokenStoreItem.IsShared() { - TokenStoreRemoveItem(&testAccountTokenStoreItem) + items := channels2items(channels) + for i := range items { + if items[i].IsShared() { + continue + } + TokenStoreRemoveItem(items[i]) } - corpAccountTokenStoreItem := WeChatCorpAccountTokenStoreItem{ - CorpId: user.WeChatCorpAccountId, - AgentSecret: user.WeChatCorpAccountAgentSecret, - AgentId: user.WeChatCorpAccountAgentId, +} + +func TokenStoreAddChannel(channel *model.Channel) { + if channel.Type != model.TypeWeChatTestAccount && channel.Type != model.TypeWeChatCorpAccount { + return } - if !corpAccountTokenStoreItem.IsShared() { - TokenStoreRemoveItem(&corpAccountTokenStoreItem) + item := channel2item(channel) + if item != nil { + TokenStoreAddItem(item) + } +} + +func TokenStoreRemoveChannel(channel *model.Channel) { + if channel.Type != model.TypeWeChatTestAccount && channel.Type != model.TypeWeChatCorpAccount { + return + } + item := channel2item(channel) + if item != nil { + TokenStoreRemoveItem(item) + } +} + +func TokenStoreUpdateChannel(newChannel *model.Channel, oldChannel *model.Channel) { + if oldChannel.Type != model.TypeWeChatTestAccount && oldChannel.Type != model.TypeWeChatCorpAccount { + return + } + if oldChannel.Type == model.TypeWeChatTestAccount { + // Only keep changed parts + if newChannel.AppId == oldChannel.AppId { + newChannel.AppId = "" + } + if newChannel.Secret == oldChannel.Secret { + newChannel.Secret = "" + } + oldItem := WeChatTestAccountTokenStoreItem{ + AppID: oldChannel.AppId, + AppSecret: oldChannel.Secret, + } + // Yeah, it's a deep copy. + newItem := oldItem + // This means the user updated those fields. + if newChannel.AppId != "" { + newItem.AppID = newChannel.AppId + } + if newChannel.Secret != "" { + newItem.AppSecret = newChannel.Secret + } + if !oldItem.IsShared() { + TokenStoreRemoveItem(&oldItem) + } + TokenStoreAddItem(&newItem) + return + } + if oldChannel.Type == model.TypeWeChatCorpAccount { + // Only keep changed parts + if newChannel.AppId == oldChannel.AppId { + newChannel.AppId = "" + } + if newChannel.Secret == oldChannel.Secret { + newChannel.Secret = "" + } + corpId, agentId, err := parseWechatCorpAccountAppId(oldChannel.AppId) + if err != nil { + common.SysError(err.Error()) + return + } + oldItem := WeChatCorpAccountTokenStoreItem{ + CorpId: corpId, + AgentSecret: oldChannel.Secret, + AgentId: agentId, + } + // Yeah, it's a deep copy. + newItem := oldItem + // This means the user updated those fields. + if newChannel.AppId != "" { + corpId, agentId, err := parseWechatCorpAccountAppId(oldChannel.AppId) + if err != nil { + common.SysError(err.Error()) + return + } + newItem.CorpId = corpId + newItem.AgentId = agentId + } + if newChannel.Secret != "" { + newItem.AgentSecret = newChannel.Secret + } + if !oldItem.IsShared() { + TokenStoreRemoveItem(&oldItem) + } + TokenStoreAddItem(&newItem) + return } } diff --git a/channel/wechat-corp-account.go b/channel/wechat-corp-account.go index 0bffa21..fe601ab 100644 --- a/channel/wechat-corp-account.go +++ b/channel/wechat-corp-account.go @@ -8,6 +8,7 @@ import ( "message-pusher/common" "message-pusher/model" "net/http" + "strings" "time" ) @@ -30,8 +31,9 @@ func (i *WeChatCorpAccountTokenStoreItem) Key() string { } func (i *WeChatCorpAccountTokenStoreItem) IsShared() bool { - return model.DB.Where("wechat_corp_account_id = ? and wechat_corp_account_agent_secret = ? and wechat_corp_account_agent_id = ?", - i.CorpId, i.AgentSecret, i.AgentId).Find(&model.User{}).RowsAffected != 1 + appId := fmt.Sprintf("%s|%s", i.CorpId, i.AgentId) + return model.DB.Where("type = ? and app_id = ? and secret = ?", + model.TypeWeChatCorpAccount, appId, i.AgentSecret).Find(&model.Channel{}).RowsAffected != 1 } func (i *WeChatCorpAccountTokenStoreItem) IsFilled() bool { @@ -95,14 +97,26 @@ type wechatCorpMessageResponse struct { ErrorMessage string `json:"errmsg"` } -func SendWeChatCorpMessage(message *model.Message, user *model.User) error { - if user.WeChatCorpAccountId == "" { - return errors.New("未配置微信企业号消息推送方式") +func parseWechatCorpAccountAppId(appId string) (string, string, error) { + parts := strings.Split(appId, "|") + if len(parts) != 2 { + return "", "", errors.New("无效的微信企业号配置") } + return parts[0], parts[1], nil +} + +func SendWeChatCorpMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { // https://developer.work.weixin.qq.com/document/path/90236 + corpId, agentId, err := parseWechatCorpAccountAppId(channel_.AppId) + if err != nil { + return err + } + userId := channel_.AccountId + clientType := channel_.Other + agentSecret := channel_.Secret messageRequest := wechatCorpMessageRequest{ - ToUser: user.WeChatCorpAccountUserId, - AgentId: user.WeChatCorpAccountAgentId, + ToUser: userId, + AgentId: agentId, } if message.To != "" { messageRequest.ToUser = message.To @@ -118,7 +132,7 @@ func SendWeChatCorpMessage(message *model.Message, user *model.User) error { messageRequest.TextCard.URL = common.ServerAddress } } else { - if user.WeChatCorpAccountClientType == "plugin" { + if clientType == "plugin" { messageRequest.MessageType = "textcard" messageRequest.TextCard.Title = message.Title messageRequest.TextCard.Description = message.Description @@ -132,7 +146,7 @@ func SendWeChatCorpMessage(message *model.Message, user *model.User) error { if err != nil { return err } - key := fmt.Sprintf("%s%s%s", user.WeChatCorpAccountId, user.WeChatCorpAccountAgentId, user.WeChatCorpAccountAgentSecret) + key := fmt.Sprintf("%s%s%s", corpId, agentId, agentSecret) accessToken := TokenStoreGetToken(key) resp, err := http.Post(fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s", accessToken), "application/json", bytes.NewBuffer(jsonData)) diff --git a/channel/wechat-test-account.go b/channel/wechat-test-account.go index 3652aee..6f6a6e8 100644 --- a/channel/wechat-test-account.go +++ b/channel/wechat-test-account.go @@ -29,8 +29,8 @@ func (i *WeChatTestAccountTokenStoreItem) Key() string { } func (i *WeChatTestAccountTokenStoreItem) IsShared() bool { - return model.DB.Where("wechat_test_account_id = ? and wechat_test_account_secret = ?", - i.AppID, i.AppSecret).Find(&model.User{}).RowsAffected != 1 + return model.DB.Where("type = ? and app_id = ? and secret = ?", + model.TypeWeChatTestAccount, i.AppID, i.AppSecret).Find(&model.Channel{}).RowsAffected != 1 } func (i *WeChatTestAccountTokenStoreItem) IsFilled() bool { @@ -88,13 +88,10 @@ type wechatTestMessageResponse struct { ErrorMessage string `json:"errmsg"` } -func SendWeChatTestMessage(message *model.Message, user *model.User) error { - if user.WeChatTestAccountId == "" { - return errors.New("未配置微信测试号消息推送方式") - } +func SendWeChatTestMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { values := wechatTestMessageRequest{ - ToUser: user.WeChatTestAccountOpenId, - TemplateId: user.WeChatTestAccountTemplateId, + ToUser: channel_.AccountId, + TemplateId: channel_.Other, URL: "", } values.Data.Text.Value = message.Description @@ -103,7 +100,7 @@ func SendWeChatTestMessage(message *model.Message, user *model.User) error { if err != nil { return err } - key := fmt.Sprintf("%s%s", user.WeChatTestAccountId, user.WeChatTestAccountSecret) + key := fmt.Sprintf("%s%s", channel_.AppId, channel_.Secret) accessToken := TokenStoreGetToken(key) resp, err := http.Post(fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", accessToken), "application/json", bytes.NewBuffer(jsonData)) diff --git a/common/constants.go b/common/constants.go index f763bf1..cc8fd5f 100644 --- a/common/constants.go +++ b/common/constants.go @@ -106,3 +106,9 @@ const ( MessageSendStatusSent = 2 MessageSendStatusFailed = 3 ) + +const ( + ChannelStatusUnknown = 0 + ChannelStatusEnabled = 1 + ChannelStatusDisabled = 2 +) diff --git a/controller/channel.go b/controller/channel.go new file mode 100644 index 0000000..d672407 --- /dev/null +++ b/controller/channel.go @@ -0,0 +1,183 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "message-pusher/channel" + "message-pusher/common" + "message-pusher/model" + "net/http" + "strconv" +) + +func GetAllChannels(c *gin.Context) { + userId := c.GetInt("id") + p, _ := strconv.Atoi(c.Query("p")) + if p < 0 { + p = 0 + } + channels, err := model.GetChannelsByUserId(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": channels, + }) + return +} + +func SearchChannels(c *gin.Context) { + userId := c.GetInt("id") + keyword := c.Query("keyword") + channels, err := model.SearchChannels(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": channels, + }) + return +} + +func GetChannel(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 + } + channel_, err := model.GetChannelById(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": channel_, + }) + return +} + +func AddChannel(c *gin.Context) { + channel_ := model.Channel{} + err := c.ShouldBindJSON(&channel_) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + if len(channel_.Name) == 0 || len(channel_.Name) > 20 { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "通道名称长度必须在1-20之间", + }) + return + } + cleanChannel := model.Channel{ + UserId: c.GetInt("id"), + Name: channel_.Name, + } + err = cleanChannel.Insert() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + channel.TokenStoreAddChannel(&cleanChannel) + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + }) + return +} + +func DeleteChannel(c *gin.Context) { + id, _ := strconv.Atoi(c.Param("id")) + userId := c.GetInt("id") + channel_, err := model.DeleteChannelById(id, userId) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + channel.TokenStoreRemoveChannel(channel_) + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + }) + return +} + +func UpdateChannel(c *gin.Context) { + userId := c.GetInt("id") + statusOnly := c.Query("status_only") + channel_ := model.Channel{} + err := c.ShouldBindJSON(&channel_) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + oldChannel, err := model.GetChannelById(channel_.Id, userId) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + cleanChannel := oldChannel + if statusOnly != "" { + cleanChannel.Status = channel_.Status + } else { + // If you add more fields, please also update channel_.Update() + cleanChannel.Type = channel_.Type + cleanChannel.Name = channel_.Name + cleanChannel.Secret = channel_.Secret + cleanChannel.AppId = channel_.AppId + cleanChannel.AccountId = channel_.AccountId + cleanChannel.URL = channel_.URL + cleanChannel.Other = channel_.Other + } + err = cleanChannel.Update() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + channel.TokenStoreUpdateChannel(cleanChannel, oldChannel) + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "", + "data": cleanChannel, + }) + return +} diff --git a/controller/message.go b/controller/message.go index 088d753..5ceb39e 100644 --- a/controller/message.go +++ b/controller/message.go @@ -65,21 +65,21 @@ func pushMessageHelper(c *gin.Context, message *model.Message) { user := model.User{Username: c.Param("username")} err := user.FillUserByUsername() if err != nil { - c.JSON(http.StatusForbidden, gin.H{ + c.JSON(http.StatusOK, gin.H{ "success": false, "message": err.Error(), }) return } if user.Status == common.UserStatusNonExisted { - c.JSON(http.StatusForbidden, gin.H{ + c.JSON(http.StatusOK, gin.H{ "success": false, "message": "用户不存在", }) return } if user.Status == common.UserStatusDisabled { - c.JSON(http.StatusForbidden, gin.H{ + c.JSON(http.StatusOK, gin.H{ "success": false, "message": "用户已被封禁", }) @@ -89,7 +89,7 @@ func pushMessageHelper(c *gin.Context, message *model.Message) { if message.Token == "" { message.Token = c.Request.Header.Get("Authorization") if message.Token == "" { - c.JSON(http.StatusForbidden, gin.H{ + c.JSON(http.StatusOK, gin.H{ "success": false, "message": "token 为空", }) @@ -97,7 +97,7 @@ func pushMessageHelper(c *gin.Context, message *model.Message) { } } if user.Token != message.Token { - c.JSON(http.StatusForbidden, gin.H{ + c.JSON(http.StatusOK, gin.H{ "success": false, "message": "无效的 token", }) @@ -110,10 +110,18 @@ func pushMessageHelper(c *gin.Context, message *model.Message) { if message.Channel == "" { message.Channel = user.Channel if message.Channel == "" { - message.Channel = channel.TypeEmail + message.Channel = model.TypeEmail } } - err = saveAndSendMessage(&user, message) + channel_, err := model.GetChannelByName(message.Channel, user.Id) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无效的渠道的名称", + }) + return + } + err = saveAndSendMessage(&user, message, channel_) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -127,7 +135,7 @@ func pushMessageHelper(c *gin.Context, message *model.Message) { }) } -func saveAndSendMessage(user *model.User, message *model.Message) error { +func saveAndSendMessage(user *model.User, message *model.Message, channel_ *model.Channel) error { message.Link = common.GetUUID() if message.URL == "" { message.URL = fmt.Sprintf("%s/message/%s", common.ServerAddress, message.Link) @@ -152,7 +160,7 @@ func saveAndSendMessage(user *model.User, message *model.Message) error { } else { message.Link = "unsaved" // This is for user to identify whether the message is saved } - err := channel.SendMessage(message, user) + err := channel.SendMessage(message, user, channel_) common.MessageCount += 1 // We don't need to use atomic here because it's not a critical value if err != nil { return err @@ -284,7 +292,11 @@ func ResendMessage(c *gin.Context) { if err != nil { return err } - err = saveAndSendMessage(user, message) + channel_, err := model.GetChannelByName(message.Channel, user.Id) + if err != nil { + return err + } + err = saveAndSendMessage(user, message, channel_) if err != nil { return err } diff --git a/controller/user.go b/controller/user.go index 7f7299e..3071a14 100644 --- a/controller/user.go +++ b/controller/user.go @@ -389,45 +389,15 @@ func UpdateSelf(c *gin.Context) { }) return } - originUser, err := model.GetUserById(c.GetInt("id"), true) - if err != nil { - c.JSON(http.StatusOK, gin.H{ - "success": false, - "message": err.Error(), - }) - return - } // White list mode. For safe :) cleanUser := model.User{ - Id: c.GetInt("id"), - Username: user.Username, - Password: user.Password, - DisplayName: user.DisplayName, - Token: user.Token, - Channel: user.Channel, - WeChatTestAccountId: user.WeChatTestAccountId, - WeChatTestAccountSecret: user.WeChatTestAccountSecret, - WeChatTestAccountTemplateId: user.WeChatTestAccountTemplateId, - WeChatTestAccountOpenId: user.WeChatTestAccountOpenId, - WeChatTestAccountVerificationToken: user.WeChatTestAccountVerificationToken, - WeChatCorpAccountId: user.WeChatCorpAccountId, - WeChatCorpAccountAgentSecret: user.WeChatCorpAccountAgentSecret, - WeChatCorpAccountAgentId: user.WeChatCorpAccountAgentId, - WeChatCorpAccountUserId: user.WeChatCorpAccountUserId, - WeChatCorpAccountClientType: user.WeChatCorpAccountClientType, - CorpWebhookURL: user.CorpWebhookURL, - LarkWebhookURL: user.LarkWebhookURL, - LarkWebhookSecret: user.LarkWebhookSecret, - DingWebhookURL: user.DingWebhookURL, - DingWebhookSecret: user.DingWebhookSecret, - BarkServer: user.BarkServer, - BarkSecret: user.BarkSecret, - ClientSecret: user.ClientSecret, - TelegramBotToken: user.TelegramBotToken, - TelegramChatId: user.TelegramChatId, - DiscordWebhookURL: user.DiscordWebhookURL, + Id: c.GetInt("id"), + Username: user.Username, + Password: user.Password, + DisplayName: user.DisplayName, + Token: user.Token, + Channel: user.Channel, } - channel.TokenStoreUpdateUser(&cleanUser, originUser) if user.Password == "$I_LOVE_U" { user.Password = "" // rollback to what it should be diff --git a/controller/websocket.go b/controller/websocket.go index 4004e07..169a3df 100644 --- a/controller/websocket.go +++ b/controller/websocket.go @@ -27,10 +27,29 @@ func RegisterClient(c *gin.Context) { } user := model.User{Username: c.Param("username")} err := user.FillUserByUsername() - if secret != user.ClientSecret { + if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, - "message": "用户名与密钥不匹配", + "message": "无效的用户名", + }) + return + } + channelName := c.Query("channel") + if channelName == "" { + channelName = "client" + } + channel_, err := model.GetChannelByName(channelName, user.Id) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无效的通道名称", + }) + return + } + if secret != channel_.Secret { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "通道名称与密钥不匹配", }) return } @@ -42,6 +61,6 @@ func RegisterClient(c *gin.Context) { }) return } - channel.RegisterClient(user.Id, conn) + channel.RegisterClient(channelName, user.Id, conn) return } diff --git a/model/channel.go b/model/channel.go new file mode 100644 index 0000000..c4f588a --- /dev/null +++ b/model/channel.go @@ -0,0 +1,106 @@ +package model + +import ( + "errors" +) + +const ( + TypeEmail = "email" + TypeWeChatTestAccount = "test" + TypeWeChatCorpAccount = "corp_app" + TypeCorp = "corp" + TypeLark = "lark" + TypeDing = "ding" + TypeTelegram = "telegram" + TypeDiscord = "discord" + TypeBark = "bark" + TypeClient = "client" + TypeNone = "none" +) + +type Channel struct { + Id int `json:"id"` + Type string `json:"type" gorm:"type:varchar(32)"` + UserId int `json:"user_id" gorm:"uniqueIndex:name_user_id"` + Name string `json:"name" gorm:"type:varchar(32);uniqueIndex:name_user_id"` + Status int `json:"status" gorm:"default:1"` // enabled, disabled + Secret string `json:"secret"` + AppId string `json:"app_id"` + AccountId string `json:"account_id"` + URL string `json:"url" gorm:"column:url"` + Other string `json:"other"` +} + +func GetChannelById(id int, userId int) (*Channel, error) { + if id == 0 || userId == 0 { + return nil, errors.New("id 或 userId 为空!") + } + c := Channel{Id: id, UserId: userId} + err := DB.Where(c).First(&c).Error + return &c, err +} + +func GetChannelByName(name string, userId int) (*Channel, error) { + if name == "" || userId == 0 { + return nil, errors.New("name 或 userId 为空!") + } + c := Channel{Name: name, UserId: userId} + err := DB.Where(c).First(&c).Error + return &c, err +} + +func GetTokenStoreChannels() (channels []*Channel, err error) { + err = DB.Where("type = ? or type = ?", TypeWeChatCorpAccount, TypeWeChatTestAccount).Find(&channels).Error + return channels, err +} + +func GetTokenStoreChannelsByUserId(userId int) (channels []*Channel, err error) { + err = DB.Where("user_id = ?", userId).Where("type = ? or type = ?", TypeWeChatCorpAccount, TypeWeChatTestAccount).Find(&channels).Error + return channels, err +} + +func GetChannelsByUserId(userId int, startIdx int, num int) (channels []*Channel, err error) { + err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&channels).Error + return channels, err +} + +func SearchChannels(userId int, keyword string) (channels []*Channel, err error) { + err = DB.Where("user_id = ?", userId).Select([]string{"id", "name"}).Where("id = ? or name LIKE", keyword, keyword+"%").Find(&channels).Error + return channels, err +} + +func DeleteChannelById(id int, userId int) (c *Channel, 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 = &Channel{Id: id, UserId: userId} + err = DB.Where(c).First(&c).Error + if err != nil { + return nil, err + } + return c, c.Delete() +} + +func (channel *Channel) Insert() error { + var err error + err = DB.Create(channel).Error + return err +} + +func (channel *Channel) UpdateStatus(status int) error { + err := DB.Model(channel).Update("status", status).Error + return err +} + +// Update Make sure your token's fields is completed, because this will update non-zero values +func (channel *Channel) Update() error { + var err error + err = DB.Model(channel).Select("type", "name", "secret", "app_id", "account_id", "url", "other").Updates(channel).Error + return err +} + +func (channel *Channel) Delete() error { + err := DB.Delete(channel).Error + return err +} diff --git a/model/main.go b/model/main.go index 8c14d09..44740dc 100644 --- a/model/main.go +++ b/model/main.go @@ -64,6 +64,10 @@ func InitDB() (err error) { if err != nil { return err } + err = db.AutoMigrate(&Channel{}) + if err != nil { + return err + } err = createRootAccountIfNeed() return err } else { diff --git a/model/user.go b/model/user.go index 5731111..591b797 100644 --- a/model/user.go +++ b/model/user.go @@ -9,41 +9,20 @@ import ( // User if you add sensitive fields, don't forget to clean them in setupLogin function. // Otherwise, the sensitive information will be saved on local storage in plain text! type User struct { - Id int `json:"id"` - Username string `json:"username" gorm:"unique;index" validate:"max=12"` - Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"` - DisplayName string `json:"display_name" gorm:"index" validate:"max=20"` - Role int `json:"role" gorm:"type:int;default:1"` // admin, common - Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled - Token string `json:"token"` - Email string `json:"email" gorm:"index" validate:"max=50"` - GitHubId string `json:"github_id" gorm:"column:github_id;index"` - WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` - VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! - Channel string `json:"channel"` - WeChatTestAccountId string `json:"wechat_test_account_id" gorm:"column:wechat_test_account_id"` - WeChatTestAccountSecret string `json:"wechat_test_account_secret" gorm:"column:wechat_test_account_secret"` - WeChatTestAccountTemplateId string `json:"wechat_test_account_template_id" gorm:"column:wechat_test_account_template_id"` - WeChatTestAccountOpenId string `json:"wechat_test_account_open_id" gorm:"column:wechat_test_account_open_id"` - WeChatTestAccountVerificationToken string `json:"wechat_test_account_verification_token" gorm:"column:wechat_test_account_verification_token"` - WeChatCorpAccountId string `json:"wechat_corp_account_id" gorm:"column:wechat_corp_account_id"` - WeChatCorpAccountAgentSecret string `json:"wechat_corp_account_agent_secret" gorm:"column:wechat_corp_account_agent_secret"` - WeChatCorpAccountAgentId string `json:"wechat_corp_account_agent_id" gorm:"column:wechat_corp_account_agent_id"` - WeChatCorpAccountUserId string `json:"wechat_corp_account_user_id" gorm:"column:wechat_corp_account_user_id"` - WeChatCorpAccountClientType string `json:"wechat_corp_account_client_type" gorm:"column:wechat_corp_account_client_type;default=plugin"` - CorpWebhookURL string `json:"corp_webhook_url" gorm:"corp_webhook_url"` - LarkWebhookURL string `json:"lark_webhook_url"` - LarkWebhookSecret string `json:"lark_webhook_secret"` - DingWebhookURL string `json:"ding_webhook_url"` - DingWebhookSecret string `json:"ding_webhook_secret"` - BarkServer string `json:"bark_server"` - BarkSecret string `json:"bark_secret"` - ClientSecret string `json:"client_secret"` - TelegramBotToken string `json:"telegram_bot_token"` - TelegramChatId string `json:"telegram_chat_id"` - DiscordWebhookURL string `json:"discord_webhook_url"` - SendEmailToOthers int `json:"send_email_to_others" gorm:"type:int;default:0"` - SaveMessageToDatabase int `json:"save_message_to_database" gorm:"type:int;default:0"` + Id int `json:"id"` + Username string `json:"username" gorm:"unique;index" validate:"max=12"` + Password string `json:"password" gorm:"not null;" validate:"min=8,max=20"` + DisplayName string `json:"display_name" gorm:"index" validate:"max=20"` + Role int `json:"role" gorm:"type:int;default:1"` // admin, common + Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled + Token string `json:"token"` + Email string `json:"email" gorm:"index" validate:"max=50"` + GitHubId string `json:"github_id" gorm:"column:github_id;index"` + WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` + VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! + Channel string `json:"channel"` + SendEmailToOthers int `json:"send_email_to_others" gorm:"type:int;default:0"` + SaveMessageToDatabase int `json:"save_message_to_database" gorm:"type:int;default:0"` } func GetMaxUserId() int { @@ -57,11 +36,6 @@ func GetAllUsers(startIdx int, num int) (users []*User, err error) { return users, err } -func GetAllUsersWithSecrets() (users []*User, err error) { - err = DB.Where("status = ?", common.UserStatusEnabled).Where("wechat_test_account_id != '' or wechat_corp_account_id != ''").Find(&users).Error - return users, err -} - func SearchUsers(keyword string) (users []*User, err error) { err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email"}).Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", keyword, keyword+"%", keyword+"%", keyword+"%").Find(&users).Error return users, err @@ -77,10 +51,7 @@ func GetUserById(id int, selectAll bool) (*User, error) { err = DB.First(&user, "id = ?", id).Error } else { err = DB.Select([]string{"id", "username", "display_name", "role", "status", "email", "wechat_id", "github_id", - "channel", "token", - "wechat_test_account_id", "wechat_test_account_template_id", "wechat_test_account_open_id", - "wechat_corp_account_id", "wechat_corp_account_agent_id", "wechat_corp_account_user_id", "wechat_corp_account_client_type", - "bark_server", "telegram_chat_id", "save_message_to_database", + "channel", "token", "save_message_to_database", }).First(&user, "id = ?", id).Error } return &user, err diff --git a/router/api-router.go b/router/api-router.go index 7943f08..a96eafe 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -64,6 +64,16 @@ func SetApiRouter(router *gin.Engine) { messageRoute.DELETE("/", middleware.RootAuth(), controller.DeleteAllMessages) messageRoute.DELETE("/:id", middleware.UserAuth(), controller.DeleteMessage) } + channelRoute := apiRouter.Group("/token") + channelRoute.Use(middleware.UserAuth()) + { + channelRoute.GET("/", controller.GetAllChannels) + channelRoute.GET("/search", controller.SearchChannels) + channelRoute.GET("/:id", controller.GetChannel) + channelRoute.POST("/", controller.AddChannel) + channelRoute.PUT("/", controller.UpdateChannel) + channelRoute.DELETE("/:id", controller.DeleteChannel) + } } pushRouter := router.Group("/push") pushRouter.Use(middleware.GlobalAPIRateLimit()) From 91725aeba7d6621134453cfb89a2bd85f1f26e77 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 6 May 2023 10:15:18 +0800 Subject: [PATCH 2/3] feat: now client part supports multiple channels for the same type (#50) --- channel/telegram.go | 2 +- common/utils.go | 5 + controller/channel.go | 14 +- controller/message.go | 4 + model/channel.go | 28 +- router/api-router.go | 2 +- web/src/App.js | 26 ++ web/src/components/ChannelsTable.js | 333 ++++++++++++++++ web/src/components/Header.js | 5 + web/src/components/MessagesTable.js | 153 +++----- web/src/constants/channel.constants.js | 38 ++ web/src/constants/index.js | 3 +- web/src/helpers/render.js | 32 ++ web/src/pages/Channel/EditChannel.js | 520 +++++++++++++++++++++++++ web/src/pages/Channel/index.js | 14 + web/src/pages/User/AddUser.js | 22 +- 16 files changed, 1070 insertions(+), 131 deletions(-) create mode 100644 web/src/components/ChannelsTable.js create mode 100644 web/src/constants/channel.constants.js create mode 100644 web/src/helpers/render.js create mode 100644 web/src/pages/Channel/EditChannel.js create mode 100644 web/src/pages/Channel/index.js diff --git a/channel/telegram.go b/channel/telegram.go index fab9a3b..ba82d69 100644 --- a/channel/telegram.go +++ b/channel/telegram.go @@ -23,7 +23,7 @@ type telegramMessageResponse struct { func SendTelegramMessage(message *model.Message, user *model.User, channel_ *model.Channel) error { // https://core.telegram.org/bots/api#sendmessage messageRequest := telegramMessageRequest{ - ChatId: channel_.AppId, + ChatId: channel_.AccountId, Text: message.Content, ParseMode: "markdown", } diff --git a/common/utils.go b/common/utils.go index 39615c9..878011d 100644 --- a/common/utils.go +++ b/common/utils.go @@ -12,6 +12,7 @@ import ( "runtime" "strconv" "strings" + "time" ) func OpenBrowser(url string) { @@ -151,3 +152,7 @@ func Markdown2HTML(markdown string) (HTML string, err error) { HTML = buf.String() return } + +func GetTimestamp() int64 { + return time.Now().Unix() +} diff --git a/controller/channel.go b/controller/channel.go index d672407..dfe6ccb 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -94,8 +94,17 @@ func AddChannel(c *gin.Context) { return } cleanChannel := model.Channel{ - UserId: c.GetInt("id"), - Name: channel_.Name, + Type: channel_.Type, + UserId: c.GetInt("id"), + Name: channel_.Name, + Description: channel_.Description, + Status: common.ChannelStatusEnabled, + Secret: channel_.Secret, + AppId: channel_.AppId, + AccountId: channel_.AccountId, + URL: channel_.URL, + Other: channel_.Other, + CreatedTime: common.GetTimestamp(), } err = cleanChannel.Insert() if err != nil { @@ -159,6 +168,7 @@ func UpdateChannel(c *gin.Context) { // If you add more fields, please also update channel_.Update() cleanChannel.Type = channel_.Type cleanChannel.Name = channel_.Name + cleanChannel.Description = channel_.Description cleanChannel.Secret = channel_.Secret cleanChannel.AppId = channel_.AppId cleanChannel.AccountId = channel_.AccountId diff --git a/controller/message.go b/controller/message.go index 5ceb39e..706205f 100644 --- a/controller/message.go +++ b/controller/message.go @@ -2,6 +2,7 @@ package controller import ( "encoding/json" + "errors" "fmt" "github.com/gin-gonic/gin" "message-pusher/channel" @@ -136,6 +137,9 @@ func pushMessageHelper(c *gin.Context, message *model.Message) { } func saveAndSendMessage(user *model.User, message *model.Message, channel_ *model.Channel) error { + if channel_.Status != common.ChannelStatusEnabled { + return errors.New("该渠道已被禁用") + } message.Link = common.GetUUID() if message.URL == "" { message.URL = fmt.Sprintf("%s/message/%s", common.ServerAddress, message.Link) diff --git a/model/channel.go b/model/channel.go index c4f588a..ed9e863 100644 --- a/model/channel.go +++ b/model/channel.go @@ -19,16 +19,18 @@ const ( ) type Channel struct { - Id int `json:"id"` - Type string `json:"type" gorm:"type:varchar(32)"` - UserId int `json:"user_id" gorm:"uniqueIndex:name_user_id"` - Name string `json:"name" gorm:"type:varchar(32);uniqueIndex:name_user_id"` - Status int `json:"status" gorm:"default:1"` // enabled, disabled - Secret string `json:"secret"` - AppId string `json:"app_id"` - AccountId string `json:"account_id"` - URL string `json:"url" gorm:"column:url"` - Other string `json:"other"` + Id int `json:"id"` + Type string `json:"type" gorm:"type:varchar(32)"` + UserId int `json:"user_id" gorm:"uniqueIndex:name_user_id"` + Name string `json:"name" gorm:"type:varchar(32);uniqueIndex:name_user_id"` + Description string `json:"description"` + Status int `json:"status" gorm:"default:1"` // enabled, disabled + Secret string `json:"secret"` + AppId string `json:"app_id"` + AccountId string `json:"account_id"` + URL string `json:"url" gorm:"column:url"` + Other string `json:"other"` + CreatedTime int64 `json:"created_time" gorm:"bigint"` } func GetChannelById(id int, userId int) (*Channel, error) { @@ -60,12 +62,12 @@ func GetTokenStoreChannelsByUserId(userId int) (channels []*Channel, err error) } func GetChannelsByUserId(userId int, startIdx int, num int) (channels []*Channel, err error) { - err = DB.Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&channels).Error + err = DB.Omit("secret").Where("user_id = ?", userId).Order("id desc").Limit(num).Offset(startIdx).Find(&channels).Error return channels, err } func SearchChannels(userId int, keyword string) (channels []*Channel, err error) { - err = DB.Where("user_id = ?", userId).Select([]string{"id", "name"}).Where("id = ? or name LIKE", keyword, keyword+"%").Find(&channels).Error + err = DB.Omit("secret").Where("user_id = ?", userId).Where("id = ? or name LIKE ?", keyword, keyword+"%").Find(&channels).Error return channels, err } @@ -96,7 +98,7 @@ func (channel *Channel) UpdateStatus(status int) error { // Update Make sure your token's fields is completed, because this will update non-zero values func (channel *Channel) Update() error { var err error - err = DB.Model(channel).Select("type", "name", "secret", "app_id", "account_id", "url", "other").Updates(channel).Error + err = DB.Model(channel).Select("type", "name", "description", "secret", "app_id", "account_id", "url", "other", "status").Updates(channel).Error return err } diff --git a/router/api-router.go b/router/api-router.go index a96eafe..515c1f3 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -64,7 +64,7 @@ func SetApiRouter(router *gin.Engine) { messageRoute.DELETE("/", middleware.RootAuth(), controller.DeleteAllMessages) messageRoute.DELETE("/:id", middleware.UserAuth(), controller.DeleteMessage) } - channelRoute := apiRouter.Group("/token") + channelRoute := apiRouter.Group("/channel") channelRoute.Use(middleware.UserAuth()) { channelRoute.GET("/", controller.GetAllChannels) diff --git a/web/src/App.js b/web/src/App.js index c11a444..aa0d38c 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -16,6 +16,8 @@ import PasswordResetConfirm from './components/PasswordResetConfirm'; import { UserContext } from './context/User'; import { StatusContext } from './context/Status'; import Message from './pages/Message'; +import Channel from './pages/Channel'; +import EditChannel from './pages/Channel/EditChannel'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); @@ -107,6 +109,30 @@ function App() { } /> + + + + } + /> + }> + + + } + /> + }> + + + } + /> { + const [channels, setChannels] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + const [user, setUser] = useState({ username: '', token: '' }); + + const loadChannels = async (startIdx) => { + const res = await API.get(`/api/channel/?p=${startIdx}`); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setChannels(data); + } else { + let newChannels = channels; + newChannels.push(...data); + setChannels(newChannels); + } + } else { + showError(message); + } + setLoading(false); + }; + + const onPaginationChange = (e, { activePage }) => { + (async () => { + if (activePage === Math.ceil(channels.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + await loadChannels(activePage - 1); + } + setActivePage(activePage); + })(); + }; + + useEffect(() => { + loadChannels(0) + .then() + .catch((reason) => { + showError(reason); + }); + loadUser() + .then() + .catch((reason) => { + showError(reason); + }); + }, []); + + const manageChannel = async (id, action, idx) => { + let data = { id }; + let res; + switch (action) { + case 'delete': + res = await API.delete(`/api/channel/${id}/`); + break; + case 'enable': + data.status = 1; + res = await API.put('/api/channel/?status_only=true', data); + break; + case 'disable': + data.status = 2; + res = await API.put('/api/channel/?status_only=true', data); + break; + } + const { success, message } = res.data; + if (success) { + showSuccess('操作成功完成!'); + let channel = res.data.data; + let newChannels = [...channels]; + let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; + if (action === 'delete') { + newChannels[realIdx].deleted = true; + } else { + newChannels[realIdx].status = channel.status; + } + setChannels(newChannels); + } else { + showError(message); + } + }; + + const renderStatus = (status) => { + switch (status) { + case 1: + return ; + case 2: + return ( + + ); + default: + return ( + + ); + } + }; + + const searchChannels = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadChannels(0); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/channel/search?keyword=${searchKeyword}`); + const { success, message, data } = res.data; + if (success) { + setChannels(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const handleKeywordChange = async (e, { value }) => { + setSearchKeyword(value.trim()); + }; + + const sortChannel = (key) => { + if (channels.length === 0) return; + setLoading(true); + let sortedChannels = [...channels]; + sortedChannels.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + if (sortedChannels[0].id === channels[0].id) { + sortedChannels.reverse(); + } + setChannels(sortedChannels); + setLoading(false); + }; + + const loadUser = async () => { + let res = await API.get(`/api/user/self`); + const { success, message, data } = res.data; + if (success) { + setUser(data); + } else { + showError(message); + } + setLoading(false); + }; + + const test = async (channel) => { + let res = await API.get( + `/push/${user.username}?token=${user.token}&channel=${channel}&title=消息推送服务&description=配置成功!` + ); + const { success, message } = res.data; + if (success) { + showSuccess('测试消息已发送'); + } else { + showError(message); + } + }; + + return ( + <> +
+ + + + + + + { + sortChannel('id'); + }} + > + ID + + { + sortChannel('name'); + }} + > + 名称 + + { + sortChannel('description'); + }} + > + 备注 + + { + sortChannel('type'); + }} + > + 类型 + + { + sortChannel('status'); + }} + > + 状态 + + { + sortChannel('created_time'); + }} + > + 创建时间 + + 操作 + + + + + {channels + .slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE + ) + .map((channel, idx) => { + if (channel.deleted) return <>; + return ( + + {channel.id} + {channel.name} + + {channel.description ? channel.description : '无备注信息'} + + {renderChannel(channel.type)} + {renderStatus(channel.status)} + + {renderTimestamp(channel.created_time)} + + +
+ + + + +
+
+
+ ); + })} +
+ + + + + + + + + +
+ + ); +}; + +export default ChannelsTable; diff --git a/web/src/components/Header.js b/web/src/components/Header.js index ec2b16c..eabfa9e 100644 --- a/web/src/components/Header.js +++ b/web/src/components/Header.js @@ -18,6 +18,11 @@ const headerButtons = [ to: '/message', icon: 'mail', }, + { + name: '通道', + to: '/channel', + icon: 'sitemap', + }, { name: '用户', to: '/user', diff --git a/web/src/components/MessagesTable.js b/web/src/components/MessagesTable.js index cd65600..5378a87 100644 --- a/web/src/components/MessagesTable.js +++ b/web/src/components/MessagesTable.js @@ -1,81 +1,16 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Button, Form, Label, Modal, Pagination, Table } from 'semantic-ui-react'; -import { API, openPage, showError, showSuccess, showWarning, timestamp2string } from '../helpers'; +import { + Button, + Form, + Label, + Modal, + Pagination, + Table, +} from 'semantic-ui-react'; +import { API, openPage, showError, showSuccess, showWarning } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; - -function renderChannel(channel) { - switch (channel) { - case 'email': - return ; - case 'test': - return ( - - ); - case 'corp_app': - return ( - - ); - case 'corp': - return ( - - ); - case 'lark': - return ( - - ); - case 'ding': - return ( - - ); - case 'bark': - return ( - - ); - case 'client': - return ( - - ); - case 'telegram': - return ( - - ); - case 'discord': - return ( - - ); - case 'none': - return ; - default: - return ; - } -} - -function renderTimestamp(timestamp) { - return ( - <> - {timestamp2string(timestamp)} - - ); -} +import { renderChannel, renderTimestamp } from '../helpers/render'; function renderStatus(status) { switch (status) { @@ -119,8 +54,8 @@ const MessagesTable = () => { title: '消息标题', description: '消息描述', content: '消息内容', - link: '' - }); // Message to be viewed + link: '', + }); // Message to be viewed const [viewModalOpen, setViewModalOpen] = useState(false); const loadMessages = async (startIdx) => { @@ -277,7 +212,7 @@ const MessagesTable = () => { autoRefreshSecondsRef.current = 10; } else { autoRefreshSecondsRef.current -= 1; - setAutoRefreshSeconds(autoRefreshSeconds => autoRefreshSeconds - 1); // Important! + setAutoRefreshSeconds((autoRefreshSeconds) => autoRefreshSeconds - 1); // Important! } }, 1000); } @@ -285,7 +220,6 @@ const MessagesTable = () => { return () => clearInterval(intervalId); }, [autoRefresh]); - return ( <>
@@ -405,16 +339,26 @@ const MessagesTable = () => { - - { - + {message.title ? message.title : '无标题'} - {message.description ?

{message.description}

: ''} + {message.description ? ( +

{message.description}

+ ) : ( + '' + )} {message.content ?

{message.content}

: ''}
- - diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js new file mode 100644 index 0000000..f2f4449 --- /dev/null +++ b/web/src/constants/channel.constants.js @@ -0,0 +1,38 @@ +export const CHANNEL_OPTIONS = [ + { key: 'email', text: '邮件', value: 'email', color: '#4285f4' }, + { key: 'test', text: '微信测试号', value: 'test', color: '#2cbb00' }, + { + key: 'corp_app', + text: '企业微信应用号', + value: 'corp_app', + color: '#5fc9ec', + }, + { key: 'corp', text: '企业微信群机器人', value: 'corp', color: '#019d82' }, + { key: 'lark', text: '飞书群机器人', value: 'lark', color: '#00d6b9' }, + { key: 'ding', text: '钉钉群机器人', value: 'ding', color: '#007fff' }, + { key: 'bark', text: 'Bark App', value: 'bark', color: '#ff3b30' }, + { + key: 'client', + text: 'WebSocket 客户端', + value: 'client', + color: '#121212', + }, + { + key: 'telegram', + text: 'Telegram 机器人', + value: 'telegram', + color: '#29a9ea', + }, + { + key: 'discord', + text: 'Discord 群机器人', + value: 'discord', + color: '#404eed', + }, + { + key: 'none', + text: '不推送', + value: 'none', + color: '#808080', + }, +]; diff --git a/web/src/constants/index.js b/web/src/constants/index.js index 8f9ba80..e83152b 100644 --- a/web/src/constants/index.js +++ b/web/src/constants/index.js @@ -1,3 +1,4 @@ export * from './toast.constants'; export * from './user.constants'; -export * from './common.constant'; \ No newline at end of file +export * from './common.constant'; +export * from './channel.constants'; \ No newline at end of file diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js new file mode 100644 index 0000000..54aa406 --- /dev/null +++ b/web/src/helpers/render.js @@ -0,0 +1,32 @@ +import { Label } from 'semantic-ui-react'; +import { timestamp2string } from './utils'; +import React from 'react'; +import { CHANNEL_OPTIONS } from '../constants'; + +let channelMap = undefined; + +export function renderChannel(key) { + if (channelMap === undefined) { + channelMap = new Map(); + CHANNEL_OPTIONS.forEach((option) => { + channelMap[option.key] = option; + }); + } + let channel = channelMap[key]; + if (channel) { + return ( + + ); + } + return ( + + ); +} + +export function renderTimestamp(timestamp) { + return <>{timestamp2string(timestamp)}; +} diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js new file mode 100644 index 0000000..1c1dc12 --- /dev/null +++ b/web/src/pages/Channel/EditChannel.js @@ -0,0 +1,520 @@ +import React, { useEffect, useState } from 'react'; +import { Button, Form, Header, Message, Segment } from 'semantic-ui-react'; +import { useParams } from 'react-router-dom'; +import { API, showError, showSuccess } from '../../helpers'; +import { CHANNEL_OPTIONS } from '../../constants'; +import axios from 'axios'; + +const EditChannel = () => { + const params = useParams(); + const channelId = params.id; + const isEditing = channelId !== undefined; + const [loading, setLoading] = useState(isEditing); + const originInputs = { + type: 'none', + name: '', + description: '', + secret: '', + app_id: '', + account_id: '', + url: '', + other: '', + corp_id: '', // only for corp_app + agent_id: '', // only for corp_app + }; + + const [inputs, setInputs] = useState(originInputs); + const { type, name, description, secret, app_id, account_id, url, other } = + inputs; + + const handleInputChange = (e, { name, value }) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; + + const loadChannel = async () => { + let res = await API.get(`/api/channel/${channelId}`); + const { success, message, data } = res.data; + if (success) { + if (data.type === 'corp_app') { + const [corp_id, agent_id] = data.app_id.split('|'); + data.corp_id = corp_id; + data.agent_id = agent_id; + } + setInputs(data); + } else { + showError(message); + } + setLoading(false); + }; + useEffect(() => { + if (isEditing) { + loadChannel().then(); + } + }, []); + + const submit = async () => { + if (!name) return; + let res = undefined; + let localInputs = { ...inputs }; + switch (inputs.type) { + case 'corp_app': + localInputs.app_id = `${inputs.corp_id}|${inputs.agent_id}`; + break; + case 'bark': + localInputs.url = 'https://api.day.app'; + } + if (isEditing) { + res = await API.put(`/api/channel/`, { + ...localInputs, + id: parseInt(channelId), + }); + } else { + res = await API.post(`/api/channel`, localInputs); + } + const { success, message } = res.data; + if (success) { + if (isEditing) { + showSuccess('通道信息更新成功!'); + } else { + showSuccess('通道创建成功!'); + setInputs(originInputs); + } + } else { + showError(message); + } + }; + + const getTelegramChatId = async () => { + if (inputs.telegram_bot_token === '') { + showError('请先输入 Telegram 机器人令牌!'); + return; + } + let res = await axios.get( + `https://api.telegram.org/bot${inputs.secret}/getUpdates` + ); + const { ok } = res.data; + if (ok) { + let result = res.data.result; + if (result.length === 0) { + showError(`请先向你的机器人发送一条任意消息!`); + } else { + let id = result[0]?.message?.chat?.id; + id = id.toString(); + setInputs((inputs) => ({ ...inputs, account_id: id })); + showSuccess('会话 ID 获取成功!'); + } + } else { + showError(`发生错误:${res.description}`); + } + }; + + const renderChannelForm = () => { + switch (type) { + case 'email': + return ( + <> + + 邮件推送方式(email)需要设置邮箱,请前往个人设置页面绑定邮箱地址,之后系统将自动为你创建邮箱推送通道。 + + + ); + case 'test': + return ( + <> + + 通过微信测试号进行推送,点击前往配置: + + 微信公众平台接口测试帐号 + + 。 +
+
+ 需要新增测试模板,模板标题推荐填写为「消息推送」,模板内容必须填写为 + {' {{'}text.DATA{'}}'}。 +
+ + + + + + + + + + ); + case 'corp_app': + return ( + <> + + 通过企业微信应用号进行推送,点击前往配置: + + 企业微信应用管理 + + 。 +
+
+ 注意,企业微信要求配置可信 IP,步骤:应用管理 -> 自建 -> 创建应用 + -> 应用设置页面下拉中找到「企业可信 IP」,点击配置 -> 设置可信域名 + -> 在「可调用 + JS-SDK、跳转小程序的可信域名」下面填写一个域名,然后点击「申请校验域名」,根据提示完成校验 + -> 之后填写服务器 IP 地址(此 IP + 地址是消息推送服务所部署在的服务器的 IP + 地址,未必是上面校验域名中记录的 IP 地址)。 +
+ + + + + + + + + + + ); + case 'corp': + return ( + <> + + 通过企业微信群机器人进行推送,配置流程:选择一个群聊 -> 设置 -> + 群机器人 -> 添加 -> 新建 -> 输入名字,点击添加 -> 点击复制 Webhook + 地址 + + + + + + ); + case 'lark': + return ( + <> + + 通过飞书群机器人进行推送,飞书桌面客户端的配置流程:选择一个群聊 + -> 设置 -> 群机器人 -> 添加机器人 -> 自定义机器人 -> 添加( + 注意选中「签名校验」)。具体参见: + + 飞书开放文档 + + + + + + + + ); + case 'ding': + return ( + <> + + 通过钉钉群机器人进行推送,钉钉桌面客户端的配置流程:选择一个群聊 + -> 群设置 -> 智能群助手 -> 添加机器人(点击右侧齿轮图标) -> + 自定义 -> 添加( + 注意选中「加密」)。具体参见: + + 钉钉开放文档 + + + + + + + + ); + case 'bark': + return ( + <> + + 通过 Bark 进行推送,下载 Bark 后按提示注册设备,之后会看到一个 + URL,例如 https://api.day.app/wrsVSDRANDOM/Body Text + ,其中 wrsVSDRANDOM 就是你的推送 key。 + + + + + + + ); + case 'client': + return ( + <> + + 通过 WebSocket + 客户端进行推送,可以使用官方客户端实现,或者根据协议自行实现。官方客户端 + + 详见此处 + + 。 + + + + + + ); + case 'telegram': + return ( + <> + + 通过 Telegram 机器人进行消息推送。首先向 + + {' '} + Bot Father{' '} + + 申请创建一个新的机器人,之后在下方输入获取到的令牌,然后点击你的机器人,随便发送一条消息,之后点击下方的「获取会话 + ID」按钮,系统将自动为你填写会话 + ID,最后点击保存按钮保存设置即可。 + + + + + + + + ); + case 'discord': + return ( + <> + + 通过 Discord 群机器人进行推送,配置流程:选择一个 channel -> 设置 + -> 整合 -> 创建 Webhook -> 点击复制 Webhook URL + + + + + + ); + case 'none': + return ( + <> + + 仅保存消息,不做推送,可以在 Web + 端查看,需要用户具有消息持久化的权限。 + + + ); + default: + return ( + <> + 未知通道类型! + + ); + } + }; + + return ( + <> + +
{isEditing ? '更新通道配置' : '新建消息通道'}
+ + + + + + + + + {renderChannelForm()} + + +
+ + ); +}; + +export default EditChannel; diff --git a/web/src/pages/Channel/index.js b/web/src/pages/Channel/index.js new file mode 100644 index 0000000..8b2fa95 --- /dev/null +++ b/web/src/pages/Channel/index.js @@ -0,0 +1,14 @@ +import React from 'react'; +import { Header, Segment } from 'semantic-ui-react'; +import ChannelsTable from '../../components/ChannelsTable'; + +const Channel = () => ( + <> + +
我的通道
+ +
+ +); + +export default Channel; diff --git a/web/src/pages/User/AddUser.js b/web/src/pages/User/AddUser.js index 73036ad..1835591 100644 --- a/web/src/pages/User/AddUser.js +++ b/web/src/pages/User/AddUser.js @@ -30,38 +30,38 @@ const AddUser = () => { return ( <> -
创建新用户账户
-
+
创建新用户账户
+ From 89fa11f962932f4c8dc601bb025c3ee3aea16f21 Mon Sep 17 00:00:00 2001 From: JustSong Date: Sat, 6 May 2023 10:16:55 +0800 Subject: [PATCH 3/3] feat: now Telegram channel support field `to` now --- channel/telegram.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/channel/telegram.go b/channel/telegram.go index ba82d69..34e6012 100644 --- a/channel/telegram.go +++ b/channel/telegram.go @@ -27,6 +27,9 @@ func SendTelegramMessage(message *model.Message, user *model.User, channel_ *mod Text: message.Content, ParseMode: "markdown", } + if message.To != "" { + messageRequest.ChatId = message.To + } if messageRequest.Text == "" { messageRequest.Text = message.Description }