✨ feat: add telegram bot (#71)
This commit is contained in:
parent
43b4ee37d9
commit
e90f4c99fc
34
common/telegram/command_aff.go
Normal file
34
common/telegram/command_aff.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/common"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandAffStart(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
user := getBindUser(b, ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.AffCode == "" {
|
||||||
|
user.AffCode = common.GetRandomString(4)
|
||||||
|
if err := user.Update(false); err != nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "系统错误,请稍后再试", nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
messae := "您可以通过分享您的邀请码来邀请朋友,每次成功邀请将获得奖励。\n\n您的邀请码是: " + user.AffCode
|
||||||
|
if common.ServerAddress != "" {
|
||||||
|
serverAddress := strings.TrimSuffix(common.ServerAddress, "/")
|
||||||
|
messae += "\n\n页面地址:" + serverAddress + "/register?aff=" + user.AffCode
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.EffectiveMessage.Reply(b, messae, nil)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
92
common/telegram/command_apikey.go
Normal file
92
common/telegram/command_apikey.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandApikeyStart(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
user := getBindUser(b, ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
message, pageParams := getApikeyList(user.Id, 1)
|
||||||
|
if pageParams == nil {
|
||||||
|
_, err := ctx.EffectiveMessage.Reply(b, message, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send APIKEY message: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ctx.EffectiveMessage.Reply(b, message, &gotgbot.SendMessageOpts{
|
||||||
|
ParseMode: "MarkdownV2",
|
||||||
|
ReplyMarkup: getPaginationInlineKeyboard(pageParams.key, pageParams.page, pageParams.total),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send APIKEY message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getApikeyList(userId, page int) (message string, pageParams *paginationParams) {
|
||||||
|
genericParams := &model.GenericParams{
|
||||||
|
PaginationParams: model.PaginationParams{
|
||||||
|
Page: page,
|
||||||
|
Size: 5,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := model.GetUserTokensList(userId, genericParams)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "系统错误,请稍后再试", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if list.Data == nil || len(*list.Data) == 0 {
|
||||||
|
return "找不到令牌", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
chatUrlTmp := ""
|
||||||
|
if common.ServerAddress != "" {
|
||||||
|
chatUrlTmp = getChatUrl()
|
||||||
|
}
|
||||||
|
|
||||||
|
message = "点击令牌可复制:\n"
|
||||||
|
|
||||||
|
for _, token := range *list.Data {
|
||||||
|
message += fmt.Sprintf("*%s* : `%s`\n", escapeText(token.Name, "MarkdownV2"), token.Key)
|
||||||
|
if chatUrlTmp != "" {
|
||||||
|
message += strings.ReplaceAll(chatUrlTmp, `setToken`, token.Key)
|
||||||
|
}
|
||||||
|
message += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
return message, getPageParams("apikey", page, genericParams.Size, int(list.TotalCount))
|
||||||
|
}
|
||||||
|
|
||||||
|
func getChatUrl() string {
|
||||||
|
serverAddress := strings.TrimSuffix(common.ServerAddress, "/")
|
||||||
|
chatNextUrl := fmt.Sprintf(`{"key":"setToken","url":"%s"}`, serverAddress)
|
||||||
|
chatNextUrl = "https://chat.oneapi.pro/#/?settings=" + url.QueryEscape(chatNextUrl)
|
||||||
|
if common.ChatLink != "" {
|
||||||
|
chatLink := strings.TrimSuffix(common.ChatLink, "/")
|
||||||
|
chatNextUrl = strings.ReplaceAll(chatNextUrl, `https://chat.oneapi.pro`, chatLink)
|
||||||
|
}
|
||||||
|
|
||||||
|
jumpUrl := fmt.Sprintf(`%s/jump?url=`, serverAddress)
|
||||||
|
|
||||||
|
amaUrl := jumpUrl + url.QueryEscape(fmt.Sprintf(`ama://set-api-key?server=%s&key=setToken`, serverAddress))
|
||||||
|
|
||||||
|
openCatUrl := jumpUrl + url.QueryEscape(fmt.Sprintf(`opencat://team/join?domain=%s&token=setToken`, serverAddress))
|
||||||
|
|
||||||
|
return fmt.Sprintf("[Next Chat](%s) [AMA](%s) [OpenCat](%s)\n", chatNextUrl, amaUrl, openCatUrl)
|
||||||
|
}
|
28
common/telegram/command_balance.go
Normal file
28
common/telegram/command_balance.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandBalanceStart(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
user := getBindUser(b, ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
quota := fmt.Sprintf("%.2f", float64(user.Quota)/500000)
|
||||||
|
usedQuota := fmt.Sprintf("%.2f", float64(user.UsedQuota)/500000)
|
||||||
|
|
||||||
|
_, err := ctx.EffectiveMessage.Reply(b, fmt.Sprintf("<b>余额:</b> $%s \n<b>已用:</b> $%s", quota, usedQuota), &gotgbot.SendMessageOpts{
|
||||||
|
ParseMode: "html",
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send balance message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
89
common/telegram/command_bind.go
Normal file
89
common/telegram/command_bind.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"one-api/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandBindInit() (handler ext.Handler) {
|
||||||
|
return handlers.NewConversation(
|
||||||
|
[]ext.Handler{handlers.NewCommand("bind", commandBindStart)},
|
||||||
|
map[string][]ext.Handler{
|
||||||
|
"token": {handlers.NewMessage(noCommands, commandBindToken)},
|
||||||
|
},
|
||||||
|
cancelConversationOpts(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandBindStart(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
user := getBindUser(b, ctx)
|
||||||
|
if user != nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "您的账户已绑定,请解邦后再试", nil)
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ctx.EffectiveMessage.Reply(b, "请输入你的访问令牌", &gotgbot.SendMessageOpts{
|
||||||
|
ParseMode: "html",
|
||||||
|
ReplyMarkup: cancelConversationInlineKeyboard(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send bind start message: %w", err)
|
||||||
|
}
|
||||||
|
return handlers.NextConversationState("token")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandBindToken(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
tgUserId := getTGUserId(b, ctx)
|
||||||
|
if tgUserId == 0 {
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
input := ctx.EffectiveMessage.Text
|
||||||
|
// 去除input前后空格
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
user := model.ValidateAccessToken(input)
|
||||||
|
if user == nil {
|
||||||
|
// If the number is not valid, try again!
|
||||||
|
ctx.EffectiveMessage.Reply(b, "Token 错误,请重试", &gotgbot.SendMessageOpts{
|
||||||
|
ParseMode: "html",
|
||||||
|
ReplyMarkup: cancelConversationInlineKeyboard(),
|
||||||
|
})
|
||||||
|
// We try the age handler again
|
||||||
|
return handlers.NextConversationState("token")
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.TelegramId != 0 {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "您的账户已绑定,请解邦后再试", nil)
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询该tg用户是否已经绑定其他账户
|
||||||
|
if model.IsTelegramIdAlreadyTaken(tgUserId) {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "该TG已绑定其他账户,请解邦后再试", nil)
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绑定
|
||||||
|
updateUser := model.User{
|
||||||
|
Id: user.Id,
|
||||||
|
TelegramId: tgUserId,
|
||||||
|
}
|
||||||
|
err := updateUser.Update(false)
|
||||||
|
if err != nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "绑定失败,请稍后再试", nil)
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = ctx.EffectiveMessage.Reply(b, "绑定成功", nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send bind token message: %w", err)
|
||||||
|
}
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
54
common/telegram/command_custom.go
Normal file
54
common/telegram/command_custom.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"one-api/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandCustom(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
command := strings.TrimSpace(ctx.EffectiveMessage.Text)
|
||||||
|
// 去除/
|
||||||
|
command = strings.TrimPrefix(command, "/")
|
||||||
|
|
||||||
|
menu, err := model.GetTelegramMenuByCommand(command)
|
||||||
|
if err != nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "系统错误,请稍后再试", nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if menu == nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "未找到该命令", nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = b.SendMessage(ctx.EffectiveSender.Id(), menu.ReplyMessage, &gotgbot.SendMessageOpts{
|
||||||
|
ParseMode: menu.ParseMode,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send %s message: %w", command, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeText(text, parseMode string) string {
|
||||||
|
switch parseMode {
|
||||||
|
case "MarkdownV2":
|
||||||
|
// Characters that need to be escaped in MarkdownV2 mode
|
||||||
|
chars := []string{"_", "*", "[", "]", "(", ")", "~", ">", "#", "+", "-", "=", "|", "{", "}", ".", "!"}
|
||||||
|
for _, char := range chars {
|
||||||
|
text = strings.ReplaceAll(text, char, "\\"+char)
|
||||||
|
}
|
||||||
|
case "HTML":
|
||||||
|
// Escape HTML special characters
|
||||||
|
text = html.EscapeString(text)
|
||||||
|
// Markdown mode does not require escaping
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
57
common/telegram/command_recharge.go
Normal file
57
common/telegram/command_recharge.go
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"one-api/model"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandRechargeInit() (handler ext.Handler) {
|
||||||
|
return handlers.NewConversation(
|
||||||
|
[]ext.Handler{handlers.NewCommand("recharge", commandRechargeStart)},
|
||||||
|
map[string][]ext.Handler{
|
||||||
|
"recharge_token": {handlers.NewMessage(noCommands, commandRechargeToken)},
|
||||||
|
},
|
||||||
|
cancelConversationOpts(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandRechargeStart(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
_, err := ctx.EffectiveMessage.Reply(b, "请输入你的兑换码", &gotgbot.SendMessageOpts{
|
||||||
|
ParseMode: "html",
|
||||||
|
ReplyMarkup: cancelConversationInlineKeyboard(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send recharge start message: %w", err)
|
||||||
|
}
|
||||||
|
return handlers.NextConversationState("recharge_token")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandRechargeToken(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
user := getBindUser(b, ctx)
|
||||||
|
if user == nil {
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
input := ctx.EffectiveMessage.Text
|
||||||
|
// 去除input前后空格
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
|
||||||
|
quota, err := model.Redeem(input, user.Id)
|
||||||
|
if err != nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "充值失败:"+err.Error(), nil)
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
money := fmt.Sprintf("%.2f", float64(quota)/500000)
|
||||||
|
_, err = ctx.EffectiveMessage.Reply(b, fmt.Sprintf("成功充值 $%s ", money), nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send recharge token message: %w", err)
|
||||||
|
}
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
29
common/telegram/command_unbind.go
Normal file
29
common/telegram/command_unbind.go
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/model"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers"
|
||||||
|
)
|
||||||
|
|
||||||
|
func commandUnbindStart(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
user := getBindUser(b, ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
updateUser := map[string]interface{}{
|
||||||
|
"telegram_id": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := model.UpdateUser(user.Id, updateUser)
|
||||||
|
if err != nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "绑定失败,请稍后再试", nil)
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.EffectiveMessage.Reply(b, "解邦成功", nil)
|
||||||
|
return nil
|
||||||
|
}
|
220
common/telegram/common.go
Normal file
220
common/telegram/common.go
Normal file
@ -0,0 +1,220 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/model"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/callbackquery"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/message"
|
||||||
|
)
|
||||||
|
|
||||||
|
var TGupdater *ext.Updater
|
||||||
|
var TGBot *gotgbot.Bot
|
||||||
|
var TGDispatcher *ext.Dispatcher
|
||||||
|
var TGWebHookSecret = ""
|
||||||
|
var TGEnabled = false
|
||||||
|
|
||||||
|
func InitTelegramBot() {
|
||||||
|
if TGEnabled {
|
||||||
|
common.SysLog("Telegram bot has been started")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Getenv("TG_BOT_API_KEY") == "" {
|
||||||
|
common.SysLog("Telegram bot is not enabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
TGBot, err = gotgbot.NewBot(os.Getenv("TG_BOT_API_KEY"), nil)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("failed to create new telegram bot: " + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
TGDispatcher = setDispatcher()
|
||||||
|
TGupdater = ext.NewUpdater(TGDispatcher, nil)
|
||||||
|
|
||||||
|
StartTelegramBot()
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartTelegramBot() {
|
||||||
|
if os.Getenv("TG_WEBHOOK_SECRET") != "" {
|
||||||
|
if common.ServerAddress == "" {
|
||||||
|
common.SysLog("Telegram bot is not enabled: Server address is not set")
|
||||||
|
StopTelegramBot()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
TGWebHookSecret = os.Getenv("TG_WEBHOOK_SECRET")
|
||||||
|
serverAddress := strings.TrimSuffix(common.ServerAddress, "/")
|
||||||
|
urlPath := fmt.Sprintf("/api/telegram/%s", os.Getenv("TG_BOT_API_KEY"))
|
||||||
|
|
||||||
|
webHookOpts := &ext.AddWebhookOpts{
|
||||||
|
SecretToken: TGWebHookSecret,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := TGupdater.AddWebhook(TGBot, urlPath, webHookOpts)
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("Telegram bot failed to add webhook:" + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = TGupdater.SetAllBotWebhooks(serverAddress, &gotgbot.SetWebhookOpts{
|
||||||
|
MaxConnections: 100,
|
||||||
|
DropPendingUpdates: true,
|
||||||
|
SecretToken: TGWebHookSecret,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("Telegram bot failed to set webhook:" + err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := TGupdater.StartPolling(TGBot, &ext.PollingOpts{
|
||||||
|
EnableWebhookDeletion: true,
|
||||||
|
DropPendingUpdates: true,
|
||||||
|
GetUpdatesOpts: &gotgbot.GetUpdatesOpts{
|
||||||
|
Timeout: 9,
|
||||||
|
RequestOpts: &gotgbot.RequestOpts{
|
||||||
|
Timeout: time.Second * 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("Telegram bot failed to start polling:" + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle, to keep updates coming in, and avoid bot stopping.
|
||||||
|
go TGupdater.Idle()
|
||||||
|
common.SysLog(fmt.Sprintf("Telegram bot %s has been started...:", TGBot.User.Username))
|
||||||
|
TGEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReloadMenuAndCommands() error {
|
||||||
|
if !TGEnabled || TGupdater == nil {
|
||||||
|
return errors.New("telegram bot is not enabled")
|
||||||
|
}
|
||||||
|
|
||||||
|
menus := getMenu()
|
||||||
|
TGBot.SetMyCommands(menus, nil)
|
||||||
|
TGDispatcher.RemoveGroup(0)
|
||||||
|
initCommand(TGDispatcher, menus)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func StopTelegramBot() {
|
||||||
|
if TGEnabled {
|
||||||
|
TGupdater.Stop()
|
||||||
|
TGupdater = nil
|
||||||
|
TGEnabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDispatcher() *ext.Dispatcher {
|
||||||
|
menus := getMenu()
|
||||||
|
TGBot.SetMyCommands(menus, nil)
|
||||||
|
|
||||||
|
// Create dispatcher.
|
||||||
|
dispatcher := ext.NewDispatcher(&ext.DispatcherOpts{
|
||||||
|
// If an error is returned by a handler, log it and continue going.
|
||||||
|
Error: func(b *gotgbot.Bot, ctx *ext.Context, err error) ext.DispatcherAction {
|
||||||
|
common.SysLog("telegram an error occurred while handling update: " + err.Error())
|
||||||
|
return ext.DispatcherActionNoop
|
||||||
|
},
|
||||||
|
MaxRoutines: ext.DefaultMaxRoutines,
|
||||||
|
})
|
||||||
|
|
||||||
|
initCommand(dispatcher, menus)
|
||||||
|
|
||||||
|
return dispatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
func initCommand(dispatcher *ext.Dispatcher, menu []gotgbot.BotCommand) {
|
||||||
|
dispatcher.AddHandler(handlers.NewCallback(callbackquery.Prefix("p:"), paginationHandler))
|
||||||
|
for _, command := range menu {
|
||||||
|
switch command.Command {
|
||||||
|
case "bind":
|
||||||
|
dispatcher.AddHandler(commandBindInit())
|
||||||
|
case "unbind":
|
||||||
|
dispatcher.AddHandler(handlers.NewCommand("unbind", commandUnbindStart))
|
||||||
|
case "balance":
|
||||||
|
dispatcher.AddHandler(handlers.NewCommand("balance", commandBalanceStart))
|
||||||
|
case "recharge":
|
||||||
|
dispatcher.AddHandler(commandRechargeInit())
|
||||||
|
case "apikey":
|
||||||
|
dispatcher.AddHandler(handlers.NewCommand("apikey", commandApikeyStart))
|
||||||
|
case "aff":
|
||||||
|
dispatcher.AddHandler(handlers.NewCommand("aff", commandAffStart))
|
||||||
|
default:
|
||||||
|
dispatcher.AddHandler(handlers.NewCommand(command.Command, commandCustom))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMenu() []gotgbot.BotCommand {
|
||||||
|
defaultMenu := GetDefaultMenu()
|
||||||
|
customMenu, err := model.GetTelegramMenus()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
common.SysLog("Failed to get custom menu, error: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(customMenu) > 0 {
|
||||||
|
// 追加自定义菜单
|
||||||
|
for _, menu := range customMenu {
|
||||||
|
defaultMenu = append(defaultMenu, gotgbot.BotCommand{Command: menu.Command, Description: menu.Description})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultMenu
|
||||||
|
}
|
||||||
|
|
||||||
|
// 菜单 1. 绑定 2. 解绑 3. 查询余额 4. 充值 5. 获取API_KEY
|
||||||
|
func GetDefaultMenu() []gotgbot.BotCommand {
|
||||||
|
return []gotgbot.BotCommand{
|
||||||
|
{Command: "bind", Description: "绑定账号"},
|
||||||
|
{Command: "unbind", Description: "解绑账号"},
|
||||||
|
{Command: "balance", Description: "查询余额"},
|
||||||
|
{Command: "recharge", Description: "充值"},
|
||||||
|
{Command: "apikey", Description: "获取API_KEY"},
|
||||||
|
{Command: "aff", Description: "获取邀请链接"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func noCommands(msg *gotgbot.Message) bool {
|
||||||
|
return message.Text(msg) && !message.Command(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTGUserId(b *gotgbot.Bot, ctx *ext.Context) int64 {
|
||||||
|
if ctx.EffectiveSender.User == nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "无法使用命令", nil)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.EffectiveSender.User.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBindUser(b *gotgbot.Bot, ctx *ext.Context) *model.User {
|
||||||
|
tgUserId := getTGUserId(b, ctx)
|
||||||
|
if tgUserId == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := model.GetUserByTelegramId(tgUserId)
|
||||||
|
if err != nil {
|
||||||
|
ctx.EffectiveMessage.Reply(b, "您的账户未绑定", nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
46
common/telegram/conversation.go
Normal file
46
common/telegram/conversation.go
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/conversation"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext/handlers/filters/callbackquery"
|
||||||
|
)
|
||||||
|
|
||||||
|
func cancelConversationInlineKeyboard() gotgbot.InlineKeyboardMarkup {
|
||||||
|
bt := gotgbot.InlineKeyboardMarkup{
|
||||||
|
InlineKeyboard: [][]gotgbot.InlineKeyboardButton{{
|
||||||
|
{Text: "取消", CallbackData: "cancel"},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
return bt
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelConversationOpts() *handlers.ConversationOpts {
|
||||||
|
return &handlers.ConversationOpts{
|
||||||
|
Exits: []ext.Handler{handlers.NewCallback(callbackquery.Equal("cancel"), cancelConversation)},
|
||||||
|
StateStorage: conversation.NewInMemoryStorage(conversation.KeyStrategySenderAndChat),
|
||||||
|
AllowReEntry: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelConversation(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
cb := ctx.Update.CallbackQuery
|
||||||
|
_, err := cb.Answer(b, &gotgbot.AnswerCallbackQueryOpts{
|
||||||
|
Text: "已取消!",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to answer start callback query: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = cb.Message.Delete(b, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send cancel message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return handlers.EndConversation()
|
||||||
|
}
|
85
common/telegram/pagination.go
Normal file
85
common/telegram/pagination.go
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
package telegram
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2"
|
||||||
|
"github.com/PaulSonOfLars/gotgbot/v2/ext"
|
||||||
|
)
|
||||||
|
|
||||||
|
type paginationParams struct {
|
||||||
|
key string
|
||||||
|
page int
|
||||||
|
total int
|
||||||
|
}
|
||||||
|
|
||||||
|
func paginationHandler(b *gotgbot.Bot, ctx *ext.Context) error {
|
||||||
|
user := getBindUser(b, ctx)
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cb := ctx.Update.CallbackQuery
|
||||||
|
parts := strings.Split(strings.TrimPrefix(ctx.CallbackQuery.Data, "p:"), ",")
|
||||||
|
page, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
cb.Answer(b, &gotgbot.AnswerCallbackQueryOpts{
|
||||||
|
Text: "参数错误!",
|
||||||
|
})
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch parts[0] {
|
||||||
|
case "apikey":
|
||||||
|
message, pageParams := getApikeyList(user.Id, page)
|
||||||
|
if pageParams == nil {
|
||||||
|
cb.Answer(b, &gotgbot.AnswerCallbackQueryOpts{
|
||||||
|
Text: message,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _, err := cb.Message.EditText(b, message, &gotgbot.EditMessageTextOpts{
|
||||||
|
ParseMode: "MarkdownV2",
|
||||||
|
ReplyMarkup: getPaginationInlineKeyboard(pageParams.key, pageParams.page, pageParams.total),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send APIKEY message: %w", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
cb.Answer(b, &gotgbot.AnswerCallbackQueryOpts{
|
||||||
|
Text: "未知的类型!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPaginationInlineKeyboard(key string, page int, total int) gotgbot.InlineKeyboardMarkup {
|
||||||
|
var bt gotgbot.InlineKeyboardMarkup
|
||||||
|
var buttons []gotgbot.InlineKeyboardButton
|
||||||
|
if page > 1 {
|
||||||
|
buttons = append(buttons, gotgbot.InlineKeyboardButton{Text: fmt.Sprintf("上一页(%d/%d)", page-1, total), CallbackData: fmt.Sprintf("p:%s,%d", key, page-1)})
|
||||||
|
}
|
||||||
|
if page < total {
|
||||||
|
buttons = append(buttons, gotgbot.InlineKeyboardButton{Text: fmt.Sprintf("下一页(%d/%d)", page+1, total), CallbackData: fmt.Sprintf("p:%s,%d", key, page+1)})
|
||||||
|
}
|
||||||
|
bt.InlineKeyboard = append(bt.InlineKeyboard, buttons)
|
||||||
|
return bt
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPageParams(key string, page, size, total_count int) *paginationParams {
|
||||||
|
// 根据总数计算总页数
|
||||||
|
total := total_count / size
|
||||||
|
if total_count%size > 0 {
|
||||||
|
total++
|
||||||
|
}
|
||||||
|
|
||||||
|
return &paginationParams{
|
||||||
|
page: page,
|
||||||
|
total: total,
|
||||||
|
key: key,
|
||||||
|
}
|
||||||
|
}
|
@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/common/telegram"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -12,6 +13,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func GetStatus(c *gin.Context) {
|
func GetStatus(c *gin.Context) {
|
||||||
|
telegram_bot := ""
|
||||||
|
if telegram.TGEnabled {
|
||||||
|
telegram_bot = telegram.TGBot.User.Username
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "",
|
"message": "",
|
||||||
@ -33,6 +39,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"chat_link": common.ChatLink,
|
"chat_link": common.ChatLink,
|
||||||
"quota_per_unit": common.QuotaPerUnit,
|
"quota_per_unit": common.QuotaPerUnit,
|
||||||
"display_in_currency": common.DisplayInCurrencyEnabled,
|
"display_in_currency": common.DisplayInCurrencyEnabled,
|
||||||
|
"telegram_bot": telegram_bot,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
131
controller/telegram.go
Normal file
131
controller/telegram.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"one-api/common"
|
||||||
|
"one-api/common/telegram"
|
||||||
|
"one-api/model"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TelegramBotWebHook(c *gin.Context) {
|
||||||
|
handlerFunc := telegram.TGupdater.GetHandlerFunc("/")
|
||||||
|
|
||||||
|
handlerFunc(c.Writer, c.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTelegramMenuList(c *gin.Context) {
|
||||||
|
var params model.GenericParams
|
||||||
|
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
list, err := model.GetTelegramMenusList(¶ms)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": list,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTelegramMenu(c *gin.Context) {
|
||||||
|
id, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
menu, err := model.GetTelegramMenuById(id)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"data": menu,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddOrUpdateTelegramMenu(c *gin.Context) {
|
||||||
|
menu := model.TelegramMenu{}
|
||||||
|
err := c.ShouldBindJSON(&menu)
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultMenu := telegram.GetDefaultMenu()
|
||||||
|
// 遍历, 禁止有相同的command
|
||||||
|
for _, v := range defaultMenu {
|
||||||
|
if v.Command == menu.Command {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, errors.New("command已存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if model.IsTelegramCommandAlreadyTaken(menu.Command, menu.Id) {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, errors.New("command已存在"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
message := "添加成功"
|
||||||
|
if menu.Id == 0 {
|
||||||
|
err = menu.Insert()
|
||||||
|
} else {
|
||||||
|
err = menu.Update()
|
||||||
|
message = "修改成功"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteTelegramMenu(c *gin.Context) {
|
||||||
|
id, _ := strconv.Atoi(c.Param("id"))
|
||||||
|
menu := model.TelegramMenu{Id: id}
|
||||||
|
err := menu.Delete()
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "删除成功",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTelegramBotStatus(c *gin.Context) {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": gin.H{
|
||||||
|
"status": telegram.TGEnabled,
|
||||||
|
"is_webhook": telegram.TGWebHookSecret != "",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReloadTelegramBot(c *gin.Context) {
|
||||||
|
err := telegram.ReloadMenuAndCommands()
|
||||||
|
if err != nil {
|
||||||
|
common.APIRespondWithError(c, http.StatusOK, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "重载成功",
|
||||||
|
})
|
||||||
|
}
|
1
go.mod
1
go.mod
@ -25,6 +25,7 @@ require (
|
|||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.24 // indirect
|
||||||
github.com/bytedance/sonic v1.9.1 // indirect
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -1,3 +1,5 @@
|
|||||||
|
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.24 h1:1T7RcpzlldaJ3qpZi0lNg/lBsfPCK+8n8Wc+R8EhAkU=
|
||||||
|
github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.24/go.mod h1:kL1v4iIjlalwm3gCYGvF4NLa3hs+aKEfRkNJvj4aoDU=
|
||||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
3
main.go
3
main.go
@ -4,6 +4,7 @@ import (
|
|||||||
"embed"
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
|
"one-api/common/telegram"
|
||||||
"one-api/controller"
|
"one-api/controller"
|
||||||
"one-api/middleware"
|
"one-api/middleware"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
@ -84,6 +85,8 @@ func main() {
|
|||||||
model.InitBatchUpdater()
|
model.InitBatchUpdater()
|
||||||
}
|
}
|
||||||
common.InitTokenEncoders()
|
common.InitTokenEncoders()
|
||||||
|
// Initialize Telegram bot
|
||||||
|
telegram.InitTelegramBot()
|
||||||
|
|
||||||
// Initialize HTTP server
|
// Initialize HTTP server
|
||||||
server := gin.New()
|
server := gin.New()
|
||||||
|
22
middleware/telegram.go
Normal file
22
middleware/telegram.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"one-api/common/telegram"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Telegram() func(c *gin.Context) {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
token := c.Param("token")
|
||||||
|
|
||||||
|
if !telegram.TGEnabled || telegram.TGWebHookSecret == "" || token == "" || token != os.Getenv("TG_BOT_API_KEY") {
|
||||||
|
c.String(404, "Page not found")
|
||||||
|
c.Abort()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
@ -40,7 +40,7 @@ var allowedChannelOrderFields = map[string]bool{
|
|||||||
"priority": true,
|
"priority": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetChannelsList(params *GenericParams) (*DataResult, error) {
|
func GetChannelsList(params *GenericParams) (*DataResult[Channel], error) {
|
||||||
var channels []*Channel
|
var channels []*Channel
|
||||||
|
|
||||||
db := DB.Omit("key")
|
db := DB.Omit("key")
|
||||||
@ -52,7 +52,7 @@ func GetChannelsList(params *GenericParams) (*DataResult, error) {
|
|||||||
db = db.Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword)
|
db = db.Where("id = ? or name LIKE ? or "+keyCol+" = ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PaginateAndOrder(db, ¶ms.PaginationParams, &channels, allowedChannelOrderFields)
|
return PaginateAndOrder[Channel](db, ¶ms.PaginationParams, &channels, allowedChannelOrderFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllChannels() ([]*Channel, error) {
|
func GetAllChannels() ([]*Channel, error) {
|
||||||
|
@ -8,6 +8,10 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type modelable interface {
|
||||||
|
any
|
||||||
|
}
|
||||||
|
|
||||||
type GenericParams struct {
|
type GenericParams struct {
|
||||||
PaginationParams
|
PaginationParams
|
||||||
Keyword string `form:"keyword"`
|
Keyword string `form:"keyword"`
|
||||||
@ -19,14 +23,14 @@ type PaginationParams struct {
|
|||||||
Order string `form:"order"`
|
Order string `form:"order"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataResult struct {
|
type DataResult[T modelable] struct {
|
||||||
Data interface{} `json:"data"`
|
Data *[]*T `json:"data"`
|
||||||
Page int `json:"page"`
|
Page int `json:"page"`
|
||||||
Size int `json:"size"`
|
Size int `json:"size"`
|
||||||
TotalCount int64 `json:"total_count"`
|
TotalCount int64 `json:"total_count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func PaginateAndOrder(db *gorm.DB, params *PaginationParams, result interface{}, allowedOrderFields map[string]bool) (*DataResult, error) {
|
func PaginateAndOrder[T modelable](db *gorm.DB, params *PaginationParams, result *[]*T, allowedOrderFields map[string]bool) (*DataResult[T], error) {
|
||||||
// 获取总数
|
// 获取总数
|
||||||
var totalCount int64
|
var totalCount int64
|
||||||
err := db.Model(result).Count(&totalCount).Error
|
err := db.Model(result).Count(&totalCount).Error
|
||||||
@ -34,8 +38,6 @@ func PaginateAndOrder(db *gorm.DB, params *PaginationParams, result interface{},
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("totalCount", totalCount)
|
|
||||||
|
|
||||||
// 分页
|
// 分页
|
||||||
if params.Page < 1 {
|
if params.Page < 1 {
|
||||||
params.Page = 1
|
params.Page = 1
|
||||||
@ -80,7 +82,7 @@ func PaginateAndOrder(db *gorm.DB, params *PaginationParams, result interface{},
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 返回结果
|
// 返回结果
|
||||||
return &DataResult{
|
return &DataResult[T]{
|
||||||
Data: result,
|
Data: result,
|
||||||
Page: params.Page,
|
Page: params.Page,
|
||||||
Size: params.Size,
|
Size: params.Size,
|
||||||
|
@ -94,7 +94,7 @@ var allowedLogsOrderFields = map[string]bool{
|
|||||||
"type": true,
|
"type": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLogsList(params *LogsListParams) (*DataResult, error) {
|
func GetLogsList(params *LogsListParams) (*DataResult[Log], error) {
|
||||||
var tx *gorm.DB
|
var tx *gorm.DB
|
||||||
var logs []*Log
|
var logs []*Log
|
||||||
|
|
||||||
@ -122,10 +122,10 @@ func GetLogsList(params *LogsListParams) (*DataResult, error) {
|
|||||||
tx = tx.Where("channel_id = ?", params.Channel)
|
tx = tx.Where("channel_id = ?", params.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PaginateAndOrder(tx, ¶ms.PaginationParams, &logs, allowedLogsOrderFields)
|
return PaginateAndOrder[Log](tx, ¶ms.PaginationParams, &logs, allowedLogsOrderFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserLogsList(userId int, params *LogsListParams) (*DataResult, error) {
|
func GetUserLogsList(userId int, params *LogsListParams) (*DataResult[Log], error) {
|
||||||
var logs []*Log
|
var logs []*Log
|
||||||
|
|
||||||
tx := DB.Where("user_id = ?", userId).Omit("id")
|
tx := DB.Where("user_id = ?", userId).Omit("id")
|
||||||
@ -146,7 +146,7 @@ func GetUserLogsList(userId int, params *LogsListParams) (*DataResult, error) {
|
|||||||
tx = tx.Where("created_at <= ?", params.EndTimestamp)
|
tx = tx.Where("created_at <= ?", params.EndTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
return PaginateAndOrder(tx, ¶ms.PaginationParams, &logs, allowedLogsOrderFields)
|
return PaginateAndOrder[Log](tx, ¶ms.PaginationParams, &logs, allowedLogsOrderFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SearchAllLogs(keyword string) (logs []*Log, err error) {
|
func SearchAllLogs(keyword string) (logs []*Log, err error) {
|
||||||
|
@ -2,14 +2,15 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gorm.io/driver/mysql"
|
|
||||||
"gorm.io/driver/postgres"
|
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/postgres"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var DB *gorm.DB
|
var DB *gorm.DB
|
||||||
@ -113,6 +114,10 @@ func InitDB() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = db.AutoMigrate(&TelegramMenu{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
common.SysLog("database migrated")
|
common.SysLog("database migrated")
|
||||||
err = createRootAccountIfNeed()
|
err = createRootAccountIfNeed()
|
||||||
return err
|
return err
|
||||||
|
@ -29,14 +29,14 @@ var allowedRedemptionslOrderFields = map[string]bool{
|
|||||||
"redeemed_time": true,
|
"redeemed_time": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRedemptionsList(params *GenericParams) (*DataResult, error) {
|
func GetRedemptionsList(params *GenericParams) (*DataResult[Redemption], error) {
|
||||||
var redemptions []*Redemption
|
var redemptions []*Redemption
|
||||||
db := DB
|
db := DB
|
||||||
if params.Keyword != "" {
|
if params.Keyword != "" {
|
||||||
db = db.Where("id = ? or name LIKE ?", common.String2Int(params.Keyword), params.Keyword+"%")
|
db = db.Where("id = ? or name LIKE ?", common.String2Int(params.Keyword), params.Keyword+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
return PaginateAndOrder(db, ¶ms.PaginationParams, &redemptions, allowedRedemptionslOrderFields)
|
return PaginateAndOrder[Redemption](db, ¶ms.PaginationParams, &redemptions, allowedRedemptionslOrderFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRedemptionById(id int) (*Redemption, error) {
|
func GetRedemptionById(id int) (*Redemption, error) {
|
||||||
|
73
model/telegram_menu.go
Normal file
73
model/telegram_menu.go
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"one-api/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TelegramMenu struct {
|
||||||
|
Id int `json:"id"`
|
||||||
|
Command string `json:"command" gorm:"type:varchar(32);uniqueIndex"`
|
||||||
|
Description string `json:"description" gorm:"type:varchar(255);default:''"`
|
||||||
|
ParseMode string `json:"parse_mode" gorm:"type:varchar(255);default:'MarkdownV2'"`
|
||||||
|
ReplyMessage string `json:"reply_message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var allowedTelegramMenusOrderFields = map[string]bool{
|
||||||
|
"id": true,
|
||||||
|
"command": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTelegramMenusList(params *GenericParams) (*DataResult[TelegramMenu], error) {
|
||||||
|
var menus []*TelegramMenu
|
||||||
|
db := DB
|
||||||
|
if params.Keyword != "" {
|
||||||
|
db = db.Where("id = ? or command LIKE ?", common.String2Int(params.Keyword), params.Keyword+"%")
|
||||||
|
}
|
||||||
|
|
||||||
|
return PaginateAndOrder[TelegramMenu](db, ¶ms.PaginationParams, &menus, allowedTelegramMenusOrderFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查询菜单列表 只查询command和description
|
||||||
|
func GetTelegramMenus() ([]*TelegramMenu, error) {
|
||||||
|
var menus []*TelegramMenu
|
||||||
|
err := DB.Select("command, description").Find(&menus).Error
|
||||||
|
return menus, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据command查询菜单
|
||||||
|
func GetTelegramMenuByCommand(command string) (*TelegramMenu, error) {
|
||||||
|
menu := &TelegramMenu{}
|
||||||
|
err := DB.Where("command = ?", command).First(menu).Error
|
||||||
|
return menu, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetTelegramMenuById(id int) (*TelegramMenu, error) {
|
||||||
|
if id == 0 {
|
||||||
|
return nil, errors.New("id 为空!")
|
||||||
|
}
|
||||||
|
telegramMenu := TelegramMenu{Id: id}
|
||||||
|
var err error = nil
|
||||||
|
err = DB.First(&telegramMenu, "id = ?", id).Error
|
||||||
|
return &telegramMenu, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsTelegramCommandAlreadyTaken(command string, id int) bool {
|
||||||
|
query := DB.Where("command = ?", command)
|
||||||
|
if id != 0 {
|
||||||
|
query = query.Not("id", id)
|
||||||
|
}
|
||||||
|
return query.Find(&TelegramMenu{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (menu *TelegramMenu) Insert() error {
|
||||||
|
return DB.Create(menu).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (menu *TelegramMenu) Update() error {
|
||||||
|
return DB.Model(menu).Updates(menu).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (menu *TelegramMenu) Delete() error {
|
||||||
|
return DB.Delete(menu).Error
|
||||||
|
}
|
@ -32,7 +32,7 @@ var allowedTokenOrderFields = map[string]bool{
|
|||||||
"used_quota": true,
|
"used_quota": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserTokensList(userId int, params *GenericParams) (*DataResult, error) {
|
func GetUserTokensList(userId int, params *GenericParams) (*DataResult[Token], error) {
|
||||||
var tokens []*Token
|
var tokens []*Token
|
||||||
db := DB.Where("user_id = ?", userId)
|
db := DB.Where("user_id = ?", userId)
|
||||||
|
|
||||||
@ -40,7 +40,13 @@ func GetUserTokensList(userId int, params *GenericParams) (*DataResult, error) {
|
|||||||
db = db.Where("name LIKE ?", params.Keyword+"%")
|
db = db.Where("name LIKE ?", params.Keyword+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
return PaginateAndOrder(db, ¶ms.PaginationParams, &tokens, allowedTokenOrderFields)
|
return PaginateAndOrder[Token](db, ¶ms.PaginationParams, &tokens, allowedTokenOrderFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取状态为可用的令牌
|
||||||
|
func GetUserEnabledTokens(userId int) (tokens []*Token, err error) {
|
||||||
|
err = DB.Where("user_id = ? and status = ?", userId, common.TokenStatusEnabled).Find(&tokens).Error
|
||||||
|
return tokens, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateUserToken(key string) (token *Token, err error) {
|
func ValidateUserToken(key string) (token *Token, err error) {
|
||||||
|
@ -21,6 +21,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"`
|
||||||
|
TelegramId int64 `json:"telegram_id" gorm:"bigint,column:telegram_id;default:0;"`
|
||||||
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database!
|
||||||
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
AccessToken string `json:"access_token" gorm:"type:char(32);column:access_token;uniqueIndex"` // this token is for system management
|
||||||
Quota int `json:"quota" gorm:"type:int;default:0"`
|
Quota int `json:"quota" gorm:"type:int;default:0"`
|
||||||
@ -32,6 +33,8 @@ type User struct {
|
|||||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserUpdates func(*User)
|
||||||
|
|
||||||
func GetMaxUserId() int {
|
func GetMaxUserId() int {
|
||||||
var user User
|
var user User
|
||||||
DB.Last(&user)
|
DB.Last(&user)
|
||||||
@ -46,14 +49,14 @@ var allowedUserOrderFields = map[string]bool{
|
|||||||
"created_time": true,
|
"created_time": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUsersList(params *GenericParams) (*DataResult, error) {
|
func GetUsersList(params *GenericParams) (*DataResult[User], error) {
|
||||||
var users []*User
|
var users []*User
|
||||||
db := DB.Omit("password")
|
db := DB.Omit("password")
|
||||||
if params.Keyword != "" {
|
if params.Keyword != "" {
|
||||||
db = db.Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword+"%", params.Keyword+"%")
|
db = db.Where("id = ? or username LIKE ? or email LIKE ? or display_name LIKE ?", common.String2Int(params.Keyword), params.Keyword+"%", params.Keyword+"%", params.Keyword+"%")
|
||||||
}
|
}
|
||||||
|
|
||||||
return PaginateAndOrder(db, ¶ms.PaginationParams, &users, allowedUserOrderFields)
|
return PaginateAndOrder[User](db, ¶ms.PaginationParams, &users, allowedUserOrderFields)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUserById(id int, selectAll bool) (*User, error) {
|
func GetUserById(id int, selectAll bool) (*User, error) {
|
||||||
@ -70,6 +73,17 @@ func GetUserById(id int, selectAll bool) (*User, error) {
|
|||||||
return &user, err
|
return &user, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetUserByTelegramId(telegramId int64) (*User, error) {
|
||||||
|
if telegramId == 0 {
|
||||||
|
return nil, errors.New("telegramId 为空!")
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
err := DB.First(&user, "telegram_id = ?", telegramId).Error
|
||||||
|
|
||||||
|
return &user, err
|
||||||
|
}
|
||||||
|
|
||||||
func GetUserIdByAffCode(affCode string) (int, error) {
|
func GetUserIdByAffCode(affCode string) (int, error) {
|
||||||
if affCode == "" {
|
if affCode == "" {
|
||||||
return 0, errors.New("affCode 为空!")
|
return 0, errors.New("affCode 为空!")
|
||||||
@ -131,6 +145,10 @@ func (user *User) Update(updatePassword bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateUser(id int, fields map[string]interface{}) error {
|
||||||
|
return DB.Model(&User{}).Where("id = ?", id).Updates(fields).Error
|
||||||
|
}
|
||||||
|
|
||||||
func (user *User) Delete() error {
|
func (user *User) Delete() error {
|
||||||
if user.Id == 0 {
|
if user.Id == 0 {
|
||||||
return errors.New("id 为空!")
|
return errors.New("id 为空!")
|
||||||
@ -216,6 +234,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool {
|
|||||||
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
return DB.Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsTelegramIdAlreadyTaken(telegramId int64) bool {
|
||||||
|
return DB.Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1
|
||||||
|
}
|
||||||
|
|
||||||
func IsUsernameAlreadyTaken(username string) bool {
|
func IsUsernameAlreadyTaken(username string) bool {
|
||||||
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
|
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
func SetApiRouter(router *gin.Engine) {
|
func SetApiRouter(router *gin.Engine) {
|
||||||
apiRouter := router.Group("/api")
|
apiRouter := router.Group("/api")
|
||||||
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
|
||||||
|
apiRouter.POST("/telegram/:token", middleware.Telegram(), controller.TelegramBotWebHook)
|
||||||
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
apiRouter.Use(middleware.GlobalAPIRateLimit())
|
||||||
{
|
{
|
||||||
apiRouter.GET("/status", controller.GetStatus)
|
apiRouter.GET("/status", controller.GetStatus)
|
||||||
@ -61,6 +62,12 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
{
|
{
|
||||||
optionRoute.GET("/", controller.GetOptions)
|
optionRoute.GET("/", controller.GetOptions)
|
||||||
optionRoute.PUT("/", controller.UpdateOption)
|
optionRoute.PUT("/", controller.UpdateOption)
|
||||||
|
optionRoute.GET("/telegram", controller.GetTelegramMenuList)
|
||||||
|
optionRoute.POST("/telegram", controller.AddOrUpdateTelegramMenu)
|
||||||
|
optionRoute.GET("/telegram/status", controller.GetTelegramBotStatus)
|
||||||
|
optionRoute.PUT("/telegram/reload", controller.ReloadTelegramBot)
|
||||||
|
optionRoute.GET("/telegram/:id", controller.GetTelegramMenu)
|
||||||
|
optionRoute.DELETE("/telegram/:id", controller.DeleteTelegramMenu)
|
||||||
}
|
}
|
||||||
channelRoute := apiRouter.Group("/channel")
|
channelRoute := apiRouter.Group("/channel")
|
||||||
channelRoute.Use(middleware.AdminAuth())
|
channelRoute.Use(middleware.AdminAuth())
|
||||||
@ -120,4 +127,5 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
analyticsRoute.GET("/redemption_period", controller.GetRedemptionStatisticsByPeriod)
|
analyticsRoute.GET("/redemption_period", controller.GetRedemptionStatisticsByPeriod)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,8 @@ import {
|
|||||||
IconGardenCart,
|
IconGardenCart,
|
||||||
IconUser,
|
IconUser,
|
||||||
IconUserScan,
|
IconUserScan,
|
||||||
IconActivity
|
IconActivity,
|
||||||
|
IconBrandTelegram
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
// constant
|
// constant
|
||||||
@ -23,7 +24,8 @@ const icons = {
|
|||||||
IconGardenCart,
|
IconGardenCart,
|
||||||
IconUser,
|
IconUser,
|
||||||
IconUserScan,
|
IconUserScan,
|
||||||
IconActivity
|
IconActivity,
|
||||||
|
IconBrandTelegram
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
|
||||||
@ -118,6 +120,15 @@ const panel = {
|
|||||||
icon: icons.IconAdjustments,
|
icon: icons.IconAdjustments,
|
||||||
breadcrumbs: false,
|
breadcrumbs: false,
|
||||||
isAdmin: true
|
isAdmin: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'telegram',
|
||||||
|
title: 'Telegram Bot',
|
||||||
|
type: 'item',
|
||||||
|
url: '/panel/telegram',
|
||||||
|
icon: icons.IconBrandTelegram,
|
||||||
|
breadcrumbs: false,
|
||||||
|
isAdmin: true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -14,6 +14,7 @@ const User = Loadable(lazy(() => import('views/User')));
|
|||||||
const Profile = Loadable(lazy(() => import('views/Profile')));
|
const Profile = Loadable(lazy(() => import('views/Profile')));
|
||||||
const NotFoundView = Loadable(lazy(() => import('views/Error')));
|
const NotFoundView = Loadable(lazy(() => import('views/Error')));
|
||||||
const Analytics = Loadable(lazy(() => import('views/Analytics')));
|
const Analytics = Loadable(lazy(() => import('views/Analytics')));
|
||||||
|
const Telegram = Loadable(lazy(() => import('views/Telegram')));
|
||||||
|
|
||||||
// dashboard routing
|
// dashboard routing
|
||||||
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
|
||||||
@ -71,6 +72,10 @@ const MainRoutes = {
|
|||||||
{
|
{
|
||||||
path: '404',
|
path: '404',
|
||||||
element: <NotFoundView />
|
element: <NotFoundView />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'telegram',
|
||||||
|
element: <Telegram />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
@ -13,6 +13,7 @@ const ResetPassword = Loadable(lazy(() => import('views/Authentication/Auth/Rese
|
|||||||
const Home = Loadable(lazy(() => import('views/Home')));
|
const Home = Loadable(lazy(() => import('views/Home')));
|
||||||
const About = Loadable(lazy(() => import('views/About')));
|
const About = Loadable(lazy(() => import('views/About')));
|
||||||
const NotFoundView = Loadable(lazy(() => import('views/Error')));
|
const NotFoundView = Loadable(lazy(() => import('views/Error')));
|
||||||
|
const Jump = Loadable(lazy(() => import('views/Jump')));
|
||||||
|
|
||||||
// ==============================|| AUTHENTICATION ROUTING ||============================== //
|
// ==============================|| AUTHENTICATION ROUTING ||============================== //
|
||||||
|
|
||||||
@ -51,6 +52,10 @@ const OtherRoutes = {
|
|||||||
{
|
{
|
||||||
path: '/404',
|
path: '/404',
|
||||||
element: <NotFoundView />
|
element: <NotFoundView />
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/jump',
|
||||||
|
element: <Jump />
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
16
web/src/views/Jump/index.js
Normal file
16
web/src/views/Jump/index.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
export default function Jump() {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(location.search);
|
||||||
|
const jump = params.get('url');
|
||||||
|
if (jump) {
|
||||||
|
window.location.href = jump;
|
||||||
|
}
|
||||||
|
}, [location]);
|
||||||
|
|
||||||
|
return <div>正在跳转中...</div>;
|
||||||
|
}
|
@ -12,11 +12,13 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
Divider
|
Divider,
|
||||||
|
Chip,
|
||||||
|
Typography
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import Grid from '@mui/material/Unstable_Grid2';
|
import Grid from '@mui/material/Unstable_Grid2';
|
||||||
import SubCard from 'ui-component/cards/SubCard';
|
import SubCard from 'ui-component/cards/SubCard';
|
||||||
import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react';
|
import { IconBrandWechat, IconBrandGithub, IconMail, IconBrandTelegram } from '@tabler/icons-react';
|
||||||
import Label from 'ui-component/Label';
|
import Label from 'ui-component/Label';
|
||||||
import { API } from 'utils/api';
|
import { API } from 'utils/api';
|
||||||
import { showError, showSuccess, onGitHubOAuthClicked, copy } from 'utils/common';
|
import { showError, showSuccess, onGitHubOAuthClicked, copy } from 'utils/common';
|
||||||
@ -141,6 +143,9 @@ export default function Profile() {
|
|||||||
<Label variant="ghost" color={inputs.email ? 'primary' : 'default'}>
|
<Label variant="ghost" color={inputs.email ? 'primary' : 'default'}>
|
||||||
<IconMail /> {inputs.email || '未绑定'}
|
<IconMail /> {inputs.email || '未绑定'}
|
||||||
</Label>
|
</Label>
|
||||||
|
<Label variant="ghost" color={inputs.telegram_id ? 'primary' : 'default'}>
|
||||||
|
<IconBrandTelegram /> {inputs.telegram_id || '未绑定'}
|
||||||
|
</Label>
|
||||||
</Stack>
|
</Stack>
|
||||||
<SubCard title="个人信息">
|
<SubCard title="个人信息">
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
@ -209,6 +214,7 @@ export default function Profile() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Grid xs={12} md={4}>
|
<Grid xs={12} md={4}>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@ -229,6 +235,35 @@ export default function Profile() {
|
|||||||
<></>
|
<></>
|
||||||
)}
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
{status.telegram_bot && ( //&& !inputs.telegram_id
|
||||||
|
<Grid xs={12} md={12}>
|
||||||
|
<Stack spacing={2}>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Alert severity="info">
|
||||||
|
<Typography variant="h3">Telegram 机器人</Typography>
|
||||||
|
<br />
|
||||||
|
<Typography variant="body1">
|
||||||
|
1. 点击下方按钮,将会在 Telegram 中打开 机器人,点击 /start 开始。
|
||||||
|
<br />
|
||||||
|
<Chip
|
||||||
|
icon={<IconBrandTelegram />}
|
||||||
|
label={'@' + status.telegram_bot}
|
||||||
|
color="primary"
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
onClick={() => window.open('https://t.me/' + status.telegram_bot, '_blank')}
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<br />
|
||||||
|
2. 向机器人发送/bind命令后,输入下方的访问令牌即可绑定。(如果没有生成,请点击下方按钮生成)
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
{/* <Typography variant=""> */}
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
</Grid>
|
</Grid>
|
||||||
</SubCard>
|
</SubCard>
|
||||||
<SubCard title="其他">
|
<SubCard title="其他">
|
||||||
|
225
web/src/views/Telegram/component/EditModal.js
Normal file
225
web/src/views/Telegram/component/EditModal.js
Normal file
@ -0,0 +1,225 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogTitle,
|
||||||
|
DialogContent,
|
||||||
|
DialogActions,
|
||||||
|
Button,
|
||||||
|
Divider,
|
||||||
|
FormControl,
|
||||||
|
InputLabel,
|
||||||
|
OutlinedInput,
|
||||||
|
FormHelperText,
|
||||||
|
Select,
|
||||||
|
MenuItem,
|
||||||
|
TextField
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import { showSuccess, showError } from 'utils/common';
|
||||||
|
import { API } from 'utils/api';
|
||||||
|
|
||||||
|
const validationSchema = Yup.object().shape({
|
||||||
|
is_edit: Yup.boolean(),
|
||||||
|
command: Yup.string().required('命令 不能为空'),
|
||||||
|
description: Yup.string().required('说明 不能为空'),
|
||||||
|
parse_mode: Yup.string().required('消息类型 不能为空'),
|
||||||
|
reply_message: Yup.string().required('消息内容 不能为空')
|
||||||
|
});
|
||||||
|
|
||||||
|
const originInputs = {
|
||||||
|
command: '',
|
||||||
|
description: '',
|
||||||
|
parse_mode: 'MarkdownV2',
|
||||||
|
reply_message: ''
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditModal = ({ open, actionId, onCancel, onOk }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const [inputs, setInputs] = useState(originInputs);
|
||||||
|
|
||||||
|
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
|
setSubmitting(true);
|
||||||
|
|
||||||
|
let res;
|
||||||
|
try {
|
||||||
|
if (values.is_edit) {
|
||||||
|
res = await API.post(`/api/option/telegram/`, { ...values, id: parseInt(actionId) });
|
||||||
|
} else {
|
||||||
|
res = await API.post(`/api/option/telegram/`, values);
|
||||||
|
}
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
if (values.is_edit) {
|
||||||
|
showSuccess('菜单更新成功!');
|
||||||
|
} else {
|
||||||
|
showSuccess('菜单创建成功!');
|
||||||
|
}
|
||||||
|
setSubmitting(false);
|
||||||
|
setStatus({ success: true });
|
||||||
|
onOk(true);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
setErrors({ submit: message });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
let res = await API.get(`/api/option/telegram/${actionId}`);
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
data.is_edit = true;
|
||||||
|
setInputs(data);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (actionId) {
|
||||||
|
load().then();
|
||||||
|
} else {
|
||||||
|
setInputs(originInputs);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [actionId]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||||
|
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>
|
||||||
|
{actionId ? '编辑菜单' : '新建菜单'}
|
||||||
|
</DialogTitle>
|
||||||
|
<Divider />
|
||||||
|
<DialogContent>
|
||||||
|
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||||
|
{({ errors, handleBlur, handleChange, handleSubmit, touched, values, isSubmitting }) => (
|
||||||
|
<form noValidate onSubmit={handleSubmit}>
|
||||||
|
<FormControl fullWidth error={Boolean(touched.command && errors.command)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-command-label">命令</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="channel-command-label"
|
||||||
|
label="命令"
|
||||||
|
type="text"
|
||||||
|
value={values.command}
|
||||||
|
name="command"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
inputProps={{ autoComplete: 'command' }}
|
||||||
|
aria-describedby="helper-text-channel-command-label"
|
||||||
|
/>
|
||||||
|
{touched.command && errors.command && (
|
||||||
|
<FormHelperText error id="helper-tex-channel-command-label">
|
||||||
|
{errors.command}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.description && errors.description)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-description-label">说明</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id="channel-description-label"
|
||||||
|
label="说明"
|
||||||
|
type="text"
|
||||||
|
value={values.description}
|
||||||
|
name="description"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
inputProps={{ autoComplete: 'description' }}
|
||||||
|
aria-describedby="helper-text-channel-description-label"
|
||||||
|
/>
|
||||||
|
{touched.description && errors.description && (
|
||||||
|
<FormHelperText error id="helper-tex-channel-description-label">
|
||||||
|
{errors.description}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.parse_mode && errors.parse_mode)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<InputLabel htmlFor="channel-parse_mode-label">消息类型</InputLabel>
|
||||||
|
<Select
|
||||||
|
id="channel-parse_mode-label"
|
||||||
|
label="消息类型"
|
||||||
|
value={values.parse_mode}
|
||||||
|
name="parse_mode"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
MenuProps={{
|
||||||
|
PaperProps: {
|
||||||
|
style: {
|
||||||
|
maxHeight: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem key="MarkdownV2" value="MarkdownV2">
|
||||||
|
{' '}
|
||||||
|
MarkdownV2{' '}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key="Markdown" value="Markdown">
|
||||||
|
{' '}
|
||||||
|
Markdown{' '}
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem key="html" value="html">
|
||||||
|
{' '}
|
||||||
|
html{' '}
|
||||||
|
</MenuItem>
|
||||||
|
</Select>
|
||||||
|
{touched.parse_mode && errors.parse_mode && (
|
||||||
|
<FormHelperText error id="helper-tex-channel-parse_mode-label">
|
||||||
|
{errors.parse_mode}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl fullWidth error={Boolean(touched.reply_message && errors.reply_message)} sx={{ ...theme.typography.otherInput }}>
|
||||||
|
<TextField
|
||||||
|
multiline
|
||||||
|
id="channel-reply_message-label"
|
||||||
|
label="消息内容"
|
||||||
|
value={values.reply_message}
|
||||||
|
name="reply_message"
|
||||||
|
onBlur={handleBlur}
|
||||||
|
onChange={handleChange}
|
||||||
|
aria-describedby="helper-text-channel-reply_message-label"
|
||||||
|
minRows={5}
|
||||||
|
placeholder="消息内容"
|
||||||
|
/>
|
||||||
|
{touched.reply_message && errors.reply_message && (
|
||||||
|
<FormHelperText error id="helper-tex-channel-reply_message-label">
|
||||||
|
{errors.reply_message}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onCancel}>取消</Button>
|
||||||
|
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||||
|
提交
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditModal;
|
||||||
|
|
||||||
|
EditModal.propTypes = {
|
||||||
|
open: PropTypes.bool,
|
||||||
|
actionId: PropTypes.number,
|
||||||
|
onCancel: PropTypes.func,
|
||||||
|
onOk: PropTypes.func
|
||||||
|
};
|
114
web/src/views/Telegram/component/TableRow.js
Normal file
114
web/src/views/Telegram/component/TableRow.js
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
TableRow,
|
||||||
|
MenuItem,
|
||||||
|
TableCell,
|
||||||
|
IconButton,
|
||||||
|
Dialog,
|
||||||
|
DialogActions,
|
||||||
|
DialogContent,
|
||||||
|
DialogContentText,
|
||||||
|
DialogTitle,
|
||||||
|
Button,
|
||||||
|
Stack
|
||||||
|
} from '@mui/material';
|
||||||
|
|
||||||
|
import { IconDotsVertical, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
export default function TelegramTableRow({ item, manageAction, handleOpenModal, setModalId }) {
|
||||||
|
const [open, setOpen] = useState(null);
|
||||||
|
const [openDelete, setOpenDelete] = useState(false);
|
||||||
|
|
||||||
|
const handleDeleteOpen = () => {
|
||||||
|
handleCloseMenu();
|
||||||
|
setOpenDelete(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteClose = () => {
|
||||||
|
setOpenDelete(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenMenu = (event) => {
|
||||||
|
setOpen(event.currentTarget);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseMenu = () => {
|
||||||
|
setOpen(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
handleCloseMenu();
|
||||||
|
await manageAction(item.id, 'delete');
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TableRow tabIndex={item.id}>
|
||||||
|
<TableCell>{item.id}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>{item.command}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>{item.description}</TableCell>
|
||||||
|
|
||||||
|
<TableCell>{item.parse_mode}</TableCell>
|
||||||
|
<TableCell>{item.reply_message}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<Stack direction="row" spacing={1}>
|
||||||
|
<IconButton onClick={handleOpenMenu} sx={{ color: 'rgb(99, 115, 129)' }}>
|
||||||
|
<IconDotsVertical />
|
||||||
|
</IconButton>
|
||||||
|
</Stack>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
|
||||||
|
<Popover
|
||||||
|
open={!!open}
|
||||||
|
anchorEl={open}
|
||||||
|
onClose={handleCloseMenu}
|
||||||
|
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||||
|
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||||
|
PaperProps={{
|
||||||
|
sx: { width: 140 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem
|
||||||
|
onClick={() => {
|
||||||
|
handleCloseMenu();
|
||||||
|
handleOpenModal();
|
||||||
|
setModalId(item.id);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconEdit style={{ marginRight: '16px' }} />
|
||||||
|
编辑
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={handleDeleteOpen} sx={{ color: 'error.main' }}>
|
||||||
|
<IconTrash style={{ marginRight: '16px' }} />
|
||||||
|
删除
|
||||||
|
</MenuItem>
|
||||||
|
</Popover>
|
||||||
|
|
||||||
|
<Dialog open={openDelete} onClose={handleDeleteClose}>
|
||||||
|
<DialogTitle>删除菜单</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText>是否删除菜单 {item.name}?</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={handleDeleteClose}>关闭</Button>
|
||||||
|
<Button onClick={handleDelete} sx={{ color: 'error.main' }} autoFocus>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
TelegramTableRow.propTypes = {
|
||||||
|
item: PropTypes.object,
|
||||||
|
manageAction: PropTypes.func,
|
||||||
|
handleOpenModal: PropTypes.func,
|
||||||
|
setModalId: PropTypes.func
|
||||||
|
};
|
270
web/src/views/Telegram/index.js
Normal file
270
web/src/views/Telegram/index.js
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { showError, showSuccess } from 'utils/common';
|
||||||
|
|
||||||
|
import Table from '@mui/material/Table';
|
||||||
|
import TableBody from '@mui/material/TableBody';
|
||||||
|
import TableContainer from '@mui/material/TableContainer';
|
||||||
|
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||||
|
import TablePagination from '@mui/material/TablePagination';
|
||||||
|
import LinearProgress from '@mui/material/LinearProgress';
|
||||||
|
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||||
|
import Toolbar from '@mui/material/Toolbar';
|
||||||
|
|
||||||
|
import { Button, Card, Box, Stack, Container, Typography, Chip, Alert } from '@mui/material';
|
||||||
|
import TelegramTableRow from './component/TableRow';
|
||||||
|
import KeywordTableHead from 'ui-component/TableHead';
|
||||||
|
import TableToolBar from 'ui-component/TableToolBar';
|
||||||
|
import { API } from 'utils/api';
|
||||||
|
import { ITEMS_PER_PAGE } from 'constants';
|
||||||
|
import { IconRefresh, IconPlus } from '@tabler/icons-react';
|
||||||
|
import EditeModal from './component/EditModal';
|
||||||
|
import { IconBrandTelegram, IconReload } from '@tabler/icons-react';
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
export default function Telegram() {
|
||||||
|
const [page, setPage] = useState(0);
|
||||||
|
const [order, setOrder] = useState('desc');
|
||||||
|
const [orderBy, setOrderBy] = useState('id');
|
||||||
|
const [rowsPerPage, setRowsPerPage] = useState(ITEMS_PER_PAGE);
|
||||||
|
const [listCount, setListCount] = useState(0);
|
||||||
|
const [searchKeyword, setSearchKeyword] = useState('');
|
||||||
|
const [searching, setSearching] = useState(false);
|
||||||
|
const [telegramMenus, setTelegramMenus] = useState([]);
|
||||||
|
const [refreshFlag, setRefreshFlag] = useState(false);
|
||||||
|
let [status, setStatus] = useState(false);
|
||||||
|
let [isWebhook, setIsWebhook] = useState(false);
|
||||||
|
|
||||||
|
const [openModal, setOpenModal] = useState(false);
|
||||||
|
const [editTelegramMenusId, setEditTelegramMenusId] = useState(0);
|
||||||
|
|
||||||
|
const handleSort = (event, id) => {
|
||||||
|
const isAsc = orderBy === id && order === 'asc';
|
||||||
|
if (id !== '') {
|
||||||
|
setOrder(isAsc ? 'desc' : 'asc');
|
||||||
|
setOrderBy(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangePage = (event, newPage) => {
|
||||||
|
setPage(newPage);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleChangeRowsPerPage = (event) => {
|
||||||
|
setPage(0);
|
||||||
|
setRowsPerPage(parseInt(event.target.value, 10));
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchMenus = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const formData = new FormData(event.target);
|
||||||
|
setPage(0);
|
||||||
|
setSearchKeyword(formData.get('keyword'));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchData = async (page, rowsPerPage, keyword, order, orderBy) => {
|
||||||
|
setSearching(true);
|
||||||
|
try {
|
||||||
|
if (orderBy) {
|
||||||
|
orderBy = order === 'desc' ? '-' + orderBy : orderBy;
|
||||||
|
}
|
||||||
|
const res = await API.get(`/api/option/telegram/`, {
|
||||||
|
params: {
|
||||||
|
page: page + 1,
|
||||||
|
size: rowsPerPage,
|
||||||
|
keyword: keyword,
|
||||||
|
order: orderBy
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const { success, message, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setListCount(data.total_count);
|
||||||
|
setTelegramMenus(data.data);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
setSearching(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reload = async () => {
|
||||||
|
try {
|
||||||
|
const res = await API.put('/api/option/telegram/reload');
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess('重载成功!');
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await API.get('/api/option/telegram/status');
|
||||||
|
const { success, data } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setStatus(data.status);
|
||||||
|
setIsWebhook(data.is_webhook);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理刷新
|
||||||
|
const handleRefresh = async () => {
|
||||||
|
setOrderBy('id');
|
||||||
|
setOrder('desc');
|
||||||
|
setRefreshFlag(!refreshFlag);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData(page, rowsPerPage, searchKeyword, order, orderBy);
|
||||||
|
}, [page, rowsPerPage, searchKeyword, order, orderBy, refreshFlag]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getStatus().then();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const manageMenus = async (id, action) => {
|
||||||
|
const url = '/api/option/telegram/';
|
||||||
|
let res;
|
||||||
|
|
||||||
|
try {
|
||||||
|
switch (action) {
|
||||||
|
case 'delete':
|
||||||
|
res = await API.delete(url + id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess('操作成功完成!');
|
||||||
|
if (action === 'delete') {
|
||||||
|
await handleRefresh();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.data;
|
||||||
|
} catch (error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOpenModal = (id) => {
|
||||||
|
setEditTelegramMenusId(id);
|
||||||
|
setOpenModal(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCloseModal = () => {
|
||||||
|
setOpenModal(false);
|
||||||
|
setEditTelegramMenusId(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleOkModal = (status) => {
|
||||||
|
if (status === true) {
|
||||||
|
handleCloseModal();
|
||||||
|
handleRefresh();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||||
|
<Typography variant="h4">Telegram Bot菜单</Typography>
|
||||||
|
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||||
|
新建
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Stack mb={5}>
|
||||||
|
<Alert severity="info">
|
||||||
|
添加修改菜单命令/说明后(如果没有修改命令和说明可以不用重载),需要重新载入菜单才能生效。
|
||||||
|
如果未查看到新菜单,请尝试杀后台后重新启动程序。
|
||||||
|
</Alert>
|
||||||
|
</Stack>
|
||||||
|
<Stack direction="row" alignItems="center" justifyContent="flex-start" mb={2} spacing={2}>
|
||||||
|
<Chip
|
||||||
|
icon={<IconBrandTelegram />}
|
||||||
|
label={(status ? '在线' : '离线') + (isWebhook ? '(Webhook)' : '(Polling)')}
|
||||||
|
color={status ? 'primary' : 'error'}
|
||||||
|
variant="outlined"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button variant="contained" size="small" endIcon={<IconReload />} onClick={reload}>
|
||||||
|
重新载入菜单
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
<Card>
|
||||||
|
<Box component="form" onSubmit={searchMenus} noValidate>
|
||||||
|
<TableToolBar placeholder={'搜索ID和命令...'} />
|
||||||
|
</Box>
|
||||||
|
<Toolbar
|
||||||
|
sx={{
|
||||||
|
textAlign: 'right',
|
||||||
|
height: 50,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
p: (theme) => theme.spacing(0, 1, 0, 3)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container>
|
||||||
|
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||||
|
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||||
|
刷新
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Container>
|
||||||
|
</Toolbar>
|
||||||
|
{searching && <LinearProgress />}
|
||||||
|
<PerfectScrollbar component="div">
|
||||||
|
<TableContainer sx={{ overflow: 'unset' }}>
|
||||||
|
<Table sx={{ minWidth: 800 }}>
|
||||||
|
<KeywordTableHead
|
||||||
|
order={order}
|
||||||
|
orderBy={orderBy}
|
||||||
|
onRequestSort={handleSort}
|
||||||
|
headLabel={[
|
||||||
|
{ id: 'id', label: 'ID', disableSort: false },
|
||||||
|
{ id: 'command', label: '命令', disableSort: false },
|
||||||
|
{ id: 'description', label: '说明', disableSort: false },
|
||||||
|
{ id: 'parse_mode', label: '回复类型', disableSort: false },
|
||||||
|
{ id: 'reply_message', label: '回复内容', disableSort: false },
|
||||||
|
{ id: 'action', label: '操作', disableSort: true }
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<TableBody>
|
||||||
|
{telegramMenus.map((row) => (
|
||||||
|
<TelegramTableRow
|
||||||
|
item={row}
|
||||||
|
manageAction={manageMenus}
|
||||||
|
key={row.id}
|
||||||
|
handleOpenModal={handleOpenModal}
|
||||||
|
setModalId={setEditTelegramMenusId}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
</PerfectScrollbar>
|
||||||
|
<TablePagination
|
||||||
|
page={page}
|
||||||
|
component="div"
|
||||||
|
count={listCount}
|
||||||
|
rowsPerPage={rowsPerPage}
|
||||||
|
onPageChange={handleChangePage}
|
||||||
|
rowsPerPageOptions={[10, 25, 30]}
|
||||||
|
onRowsPerPageChange={handleChangeRowsPerPage}
|
||||||
|
showFirstButton
|
||||||
|
showLastButton
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
<EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} actionId={editTelegramMenusId} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user