From e90f4c99fcc69ce832f1f19aa0303acc7b584abb Mon Sep 17 00:00:00 2001
From: Buer <42402987+MartialBE@users.noreply.github.com>
Date: Fri, 23 Feb 2024 18:24:25 +0800
Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20telegram=20bot=20(#71?=
=?UTF-8?q?)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
common/telegram/command_aff.go | 34 +++
common/telegram/command_apikey.go | 92 ++++++
common/telegram/command_balance.go | 28 ++
common/telegram/command_bind.go | 89 ++++++
common/telegram/command_custom.go | 54 ++++
common/telegram/command_recharge.go | 57 ++++
common/telegram/command_unbind.go | 29 ++
common/telegram/common.go | 220 ++++++++++++++
common/telegram/conversation.go | 46 +++
common/telegram/pagination.go | 85 ++++++
controller/misc.go | 7 +
controller/telegram.go | 131 +++++++++
go.mod | 1 +
go.sum | 2 +
main.go | 3 +
middleware/telegram.go | 22 ++
model/channel.go | 4 +-
model/common.go | 20 +-
model/log.go | 8 +-
model/main.go | 13 +-
model/redemption.go | 4 +-
model/telegram_menu.go | 73 +++++
model/token.go | 10 +-
model/user.go | 26 +-
router/api-router.go | 8 +
web/src/menu-items/panel.js | 15 +-
web/src/routes/MainRoutes.js | 5 +
web/src/routes/OtherRoutes.js | 5 +
web/src/views/Jump/index.js | 16 ++
web/src/views/Profile/index.js | 39 ++-
web/src/views/Telegram/component/EditModal.js | 225 +++++++++++++++
web/src/views/Telegram/component/TableRow.js | 114 ++++++++
web/src/views/Telegram/index.js | 270 ++++++++++++++++++
33 files changed, 1726 insertions(+), 29 deletions(-)
create mode 100644 common/telegram/command_aff.go
create mode 100644 common/telegram/command_apikey.go
create mode 100644 common/telegram/command_balance.go
create mode 100644 common/telegram/command_bind.go
create mode 100644 common/telegram/command_custom.go
create mode 100644 common/telegram/command_recharge.go
create mode 100644 common/telegram/command_unbind.go
create mode 100644 common/telegram/common.go
create mode 100644 common/telegram/conversation.go
create mode 100644 common/telegram/pagination.go
create mode 100644 controller/telegram.go
create mode 100644 middleware/telegram.go
create mode 100644 model/telegram_menu.go
create mode 100644 web/src/views/Jump/index.js
create mode 100644 web/src/views/Telegram/component/EditModal.js
create mode 100644 web/src/views/Telegram/component/TableRow.js
create mode 100644 web/src/views/Telegram/index.js
diff --git a/common/telegram/command_aff.go b/common/telegram/command_aff.go
new file mode 100644
index 00000000..fd15a2a6
--- /dev/null
+++ b/common/telegram/command_aff.go
@@ -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
+}
diff --git a/common/telegram/command_apikey.go b/common/telegram/command_apikey.go
new file mode 100644
index 00000000..c5d2e4da
--- /dev/null
+++ b/common/telegram/command_apikey.go
@@ -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)
+}
diff --git a/common/telegram/command_balance.go b/common/telegram/command_balance.go
new file mode 100644
index 00000000..481a4264
--- /dev/null
+++ b/common/telegram/command_balance.go
@@ -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("余额: $%s \n已用: $%s", quota, usedQuota), &gotgbot.SendMessageOpts{
+ ParseMode: "html",
+ })
+
+ if err != nil {
+ return fmt.Errorf("failed to send balance message: %w", err)
+ }
+
+ return err
+}
diff --git a/common/telegram/command_bind.go b/common/telegram/command_bind.go
new file mode 100644
index 00000000..8519f064
--- /dev/null
+++ b/common/telegram/command_bind.go
@@ -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()
+}
diff --git a/common/telegram/command_custom.go b/common/telegram/command_custom.go
new file mode 100644
index 00000000..6b7fb62e
--- /dev/null
+++ b/common/telegram/command_custom.go
@@ -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
+}
diff --git a/common/telegram/command_recharge.go b/common/telegram/command_recharge.go
new file mode 100644
index 00000000..e26cb1f6
--- /dev/null
+++ b/common/telegram/command_recharge.go
@@ -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()
+}
diff --git a/common/telegram/command_unbind.go b/common/telegram/command_unbind.go
new file mode 100644
index 00000000..da5fa5e3
--- /dev/null
+++ b/common/telegram/command_unbind.go
@@ -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
+}
diff --git a/common/telegram/common.go b/common/telegram/common.go
new file mode 100644
index 00000000..4d92cee4
--- /dev/null
+++ b/common/telegram/common.go
@@ -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
+}
diff --git a/common/telegram/conversation.go b/common/telegram/conversation.go
new file mode 100644
index 00000000..45016d30
--- /dev/null
+++ b/common/telegram/conversation.go
@@ -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()
+}
diff --git a/common/telegram/pagination.go b/common/telegram/pagination.go
new file mode 100644
index 00000000..c38faff0
--- /dev/null
+++ b/common/telegram/pagination.go
@@ -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,
+ }
+}
diff --git a/controller/misc.go b/controller/misc.go
index 4940bcf7..fd723a49 100644
--- a/controller/misc.go
+++ b/controller/misc.go
@@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"one-api/common"
+ "one-api/common/telegram"
"one-api/model"
"strings"
@@ -12,6 +13,11 @@ import (
)
func GetStatus(c *gin.Context) {
+ telegram_bot := ""
+ if telegram.TGEnabled {
+ telegram_bot = telegram.TGBot.User.Username
+ }
+
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "",
@@ -33,6 +39,7 @@ func GetStatus(c *gin.Context) {
"chat_link": common.ChatLink,
"quota_per_unit": common.QuotaPerUnit,
"display_in_currency": common.DisplayInCurrencyEnabled,
+ "telegram_bot": telegram_bot,
},
})
}
diff --git a/controller/telegram.go b/controller/telegram.go
new file mode 100644
index 00000000..500ebe5e
--- /dev/null
+++ b/controller/telegram.go
@@ -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": "重载成功",
+ })
+}
diff --git a/go.mod b/go.mod
index fbb34d58..d8792ba3 100644
--- a/go.mod
+++ b/go.mod
@@ -25,6 +25,7 @@ require (
)
require (
+ github.com/PaulSonOfLars/gotgbot/v2 v2.0.0-rc.24 // indirect
github.com/bytedance/sonic v1.9.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
diff --git a/go.sum b/go.sum
index 2517022f..74696c0e 100644
--- a/go.sum
+++ b/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.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
diff --git a/main.go b/main.go
index 4c897d51..16ef5e48 100644
--- a/main.go
+++ b/main.go
@@ -4,6 +4,7 @@ import (
"embed"
"fmt"
"one-api/common"
+ "one-api/common/telegram"
"one-api/controller"
"one-api/middleware"
"one-api/model"
@@ -84,6 +85,8 @@ func main() {
model.InitBatchUpdater()
}
common.InitTokenEncoders()
+ // Initialize Telegram bot
+ telegram.InitTelegramBot()
// Initialize HTTP server
server := gin.New()
diff --git a/middleware/telegram.go b/middleware/telegram.go
new file mode 100644
index 00000000..d6d6559a
--- /dev/null
+++ b/middleware/telegram.go
@@ -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()
+ }
+}
diff --git a/model/channel.go b/model/channel.go
index 29c5b038..548a8c96 100644
--- a/model/channel.go
+++ b/model/channel.go
@@ -40,7 +40,7 @@ var allowedChannelOrderFields = map[string]bool{
"priority": true,
}
-func GetChannelsList(params *GenericParams) (*DataResult, error) {
+func GetChannelsList(params *GenericParams) (*DataResult[Channel], error) {
var channels []*Channel
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)
}
- return PaginateAndOrder(db, ¶ms.PaginationParams, &channels, allowedChannelOrderFields)
+ return PaginateAndOrder[Channel](db, ¶ms.PaginationParams, &channels, allowedChannelOrderFields)
}
func GetAllChannels() ([]*Channel, error) {
diff --git a/model/common.go b/model/common.go
index 37fcd739..84e1e422 100644
--- a/model/common.go
+++ b/model/common.go
@@ -8,6 +8,10 @@ import (
"gorm.io/gorm"
)
+type modelable interface {
+ any
+}
+
type GenericParams struct {
PaginationParams
Keyword string `form:"keyword"`
@@ -19,14 +23,14 @@ type PaginationParams struct {
Order string `form:"order"`
}
-type DataResult struct {
- Data interface{} `json:"data"`
- Page int `json:"page"`
- Size int `json:"size"`
- TotalCount int64 `json:"total_count"`
+type DataResult[T modelable] struct {
+ Data *[]*T `json:"data"`
+ Page int `json:"page"`
+ Size int `json:"size"`
+ 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
err := db.Model(result).Count(&totalCount).Error
@@ -34,8 +38,6 @@ func PaginateAndOrder(db *gorm.DB, params *PaginationParams, result interface{},
return nil, err
}
- fmt.Println("totalCount", totalCount)
-
// 分页
if 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,
Page: params.Page,
Size: params.Size,
diff --git a/model/log.go b/model/log.go
index a4141952..a3cc46ad 100644
--- a/model/log.go
+++ b/model/log.go
@@ -94,7 +94,7 @@ var allowedLogsOrderFields = map[string]bool{
"type": true,
}
-func GetLogsList(params *LogsListParams) (*DataResult, error) {
+func GetLogsList(params *LogsListParams) (*DataResult[Log], error) {
var tx *gorm.DB
var logs []*Log
@@ -122,10 +122,10 @@ func GetLogsList(params *LogsListParams) (*DataResult, error) {
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
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)
}
- return PaginateAndOrder(tx, ¶ms.PaginationParams, &logs, allowedLogsOrderFields)
+ return PaginateAndOrder[Log](tx, ¶ms.PaginationParams, &logs, allowedLogsOrderFields)
}
func SearchAllLogs(keyword string) (logs []*Log, err error) {
diff --git a/model/main.go b/model/main.go
index bfd6888b..b4a90338 100644
--- a/model/main.go
+++ b/model/main.go
@@ -2,14 +2,15 @@ package model
import (
"fmt"
- "gorm.io/driver/mysql"
- "gorm.io/driver/postgres"
- "gorm.io/driver/sqlite"
- "gorm.io/gorm"
"one-api/common"
"os"
"strings"
"time"
+
+ "gorm.io/driver/mysql"
+ "gorm.io/driver/postgres"
+ "gorm.io/driver/sqlite"
+ "gorm.io/gorm"
)
var DB *gorm.DB
@@ -113,6 +114,10 @@ func InitDB() (err error) {
if err != nil {
return err
}
+ err = db.AutoMigrate(&TelegramMenu{})
+ if err != nil {
+ return err
+ }
common.SysLog("database migrated")
err = createRootAccountIfNeed()
return err
diff --git a/model/redemption.go b/model/redemption.go
index a5389102..07df8d80 100644
--- a/model/redemption.go
+++ b/model/redemption.go
@@ -29,14 +29,14 @@ var allowedRedemptionslOrderFields = map[string]bool{
"redeemed_time": true,
}
-func GetRedemptionsList(params *GenericParams) (*DataResult, error) {
+func GetRedemptionsList(params *GenericParams) (*DataResult[Redemption], error) {
var redemptions []*Redemption
db := DB
if 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) {
diff --git a/model/telegram_menu.go b/model/telegram_menu.go
new file mode 100644
index 00000000..451fda3e
--- /dev/null
+++ b/model/telegram_menu.go
@@ -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
+}
diff --git a/model/token.go b/model/token.go
index 74eae34b..1e4e3123 100644
--- a/model/token.go
+++ b/model/token.go
@@ -32,7 +32,7 @@ var allowedTokenOrderFields = map[string]bool{
"used_quota": true,
}
-func GetUserTokensList(userId int, params *GenericParams) (*DataResult, error) {
+func GetUserTokensList(userId int, params *GenericParams) (*DataResult[Token], error) {
var tokens []*Token
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+"%")
}
- 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) {
diff --git a/model/user.go b/model/user.go
index 50a6d5a8..3adb7b83 100644
--- a/model/user.go
+++ b/model/user.go
@@ -21,6 +21,7 @@ type User struct {
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"`
+ 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!
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"`
@@ -32,6 +33,8 @@ type User struct {
CreatedTime int64 `json:"created_time" gorm:"bigint"`
}
+type UserUpdates func(*User)
+
func GetMaxUserId() int {
var user User
DB.Last(&user)
@@ -46,14 +49,14 @@ var allowedUserOrderFields = map[string]bool{
"created_time": true,
}
-func GetUsersList(params *GenericParams) (*DataResult, error) {
+func GetUsersList(params *GenericParams) (*DataResult[User], error) {
var users []*User
db := DB.Omit("password")
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+"%")
}
- return PaginateAndOrder(db, ¶ms.PaginationParams, &users, allowedUserOrderFields)
+ return PaginateAndOrder[User](db, ¶ms.PaginationParams, &users, allowedUserOrderFields)
}
func GetUserById(id int, selectAll bool) (*User, error) {
@@ -70,6 +73,17 @@ func GetUserById(id int, selectAll bool) (*User, error) {
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) {
if affCode == "" {
return 0, errors.New("affCode 为空!")
@@ -131,6 +145,10 @@ func (user *User) Update(updatePassword bool) error {
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 {
if user.Id == 0 {
return errors.New("id 为空!")
@@ -216,6 +234,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool {
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 {
return DB.Where("username = ?", username).Find(&User{}).RowsAffected == 1
}
diff --git a/router/api-router.go b/router/api-router.go
index f602a68a..f1bd65f2 100644
--- a/router/api-router.go
+++ b/router/api-router.go
@@ -11,6 +11,7 @@ import (
func SetApiRouter(router *gin.Engine) {
apiRouter := router.Group("/api")
apiRouter.Use(gzip.Gzip(gzip.DefaultCompression))
+ apiRouter.POST("/telegram/:token", middleware.Telegram(), controller.TelegramBotWebHook)
apiRouter.Use(middleware.GlobalAPIRateLimit())
{
apiRouter.GET("/status", controller.GetStatus)
@@ -61,6 +62,12 @@ func SetApiRouter(router *gin.Engine) {
{
optionRoute.GET("/", controller.GetOptions)
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.Use(middleware.AdminAuth())
@@ -120,4 +127,5 @@ func SetApiRouter(router *gin.Engine) {
analyticsRoute.GET("/redemption_period", controller.GetRedemptionStatisticsByPeriod)
}
}
+
}
diff --git a/web/src/menu-items/panel.js b/web/src/menu-items/panel.js
index c4ee6b28..9bc945f7 100644
--- a/web/src/menu-items/panel.js
+++ b/web/src/menu-items/panel.js
@@ -9,7 +9,8 @@ import {
IconGardenCart,
IconUser,
IconUserScan,
- IconActivity
+ IconActivity,
+ IconBrandTelegram
} from '@tabler/icons-react';
// constant
@@ -23,7 +24,8 @@ const icons = {
IconGardenCart,
IconUser,
IconUserScan,
- IconActivity
+ IconActivity,
+ IconBrandTelegram
};
// ==============================|| DASHBOARD MENU ITEMS ||============================== //
@@ -118,6 +120,15 @@ const panel = {
icon: icons.IconAdjustments,
breadcrumbs: false,
isAdmin: true
+ },
+ {
+ id: 'telegram',
+ title: 'Telegram Bot',
+ type: 'item',
+ url: '/panel/telegram',
+ icon: icons.IconBrandTelegram,
+ breadcrumbs: false,
+ isAdmin: true
}
]
};
diff --git a/web/src/routes/MainRoutes.js b/web/src/routes/MainRoutes.js
index 9cd26b0e..f783cf21 100644
--- a/web/src/routes/MainRoutes.js
+++ b/web/src/routes/MainRoutes.js
@@ -14,6 +14,7 @@ const User = Loadable(lazy(() => import('views/User')));
const Profile = Loadable(lazy(() => import('views/Profile')));
const NotFoundView = Loadable(lazy(() => import('views/Error')));
const Analytics = Loadable(lazy(() => import('views/Analytics')));
+const Telegram = Loadable(lazy(() => import('views/Telegram')));
// dashboard routing
const Dashboard = Loadable(lazy(() => import('views/Dashboard')));
@@ -71,6 +72,10 @@ const MainRoutes = {
{
path: '404',
element: