Files
epusdt/src/telegram/handle.go
T
line-6000 6bb47d4b00 feat: add admin panel with multi-chain support and notification system
- Add full admin REST API: auth, API keys, chains, chain tokens, wallets,
  orders, RPC nodes, settings, dashboard stats, notifications
- Add JWT-based admin authentication and API key auth middleware
- Add multi-chain listeners: BSC, ETH, Polygon, Plasma, EVM WebSocket
- Add RPC node health check job with automatic failover
- Add Telegram notification channel for order events
- Add installer for first-run database initialization
- Add new DB models: admin_user, api_key, chain, chain_token,
  rpc_node, settings, notification_channel
- Add order statistics aggregation (daily/monthly/by-chain)
- Serve built admin SPA from embedded www/ assets
- Add comprehensive test coverage for all new features
2026-04-22 02:28:31 +08:00

275 lines
6.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package telegram
import (
"fmt"
"sort"
"strings"
"sync"
"time"
"github.com/assimon/luuu/model/data"
"github.com/assimon/luuu/model/mdb"
"github.com/gookit/goutil/mathutil"
"github.com/gookit/goutil/strutil"
tb "gopkg.in/telebot.v3"
)
const (
ReplaySelectNetwork = "请选择要添加的钱包网络"
ReplayAddWallet = "请发送 %s 网络收款地址"
pendingWalletAddressTTL = 5 * time.Minute
)
type pendingWalletAddressState struct {
RequestedAt time.Time
Network string
}
var pendingWalletAddressUsers sync.Map
func OnTextMessageHandle(c tb.Context) error {
msg := c.Message()
if msg == nil {
return nil
}
sender := c.Sender()
senderID := int64(0)
if sender != nil {
senderID = sender.ID
}
isReplyFlow := msg.ReplyTo != nil && strings.HasPrefix(msg.ReplyTo.Text, "请发送 ")
isPendingFlow := isWalletAddressPending(senderID)
if !isReplyFlow && !isPendingFlow {
return nil
}
if isReplyFlow {
defer bots.Delete(msg.ReplyTo)
}
msgText := strings.TrimSpace(msg.Text)
state, _ := getPendingWalletAddressState(senderID)
if state.Network == "" {
_ = c.Send("请先选择网络,再发送地址。")
return nil
}
var err error
if !isValidAddressByNetwork(state.Network, msgText) {
_ = c.Send(fmt.Sprintf("钱包 [%s] 添加失败:不是合法的 %s 地址", msgText, strings.ToUpper(state.Network)))
return nil
}
storeAddress := normalizeWalletAddressByNetwork(state.Network, msgText)
_, err = data.AddWalletAddressWithNetwork(state.Network, storeAddress)
if err != nil {
return c.Send(err.Error())
}
pendingWalletAddressUsers.Delete(senderID)
_ = c.Send(fmt.Sprintf("钱包 [%s] 添加成功(%s", storeAddress, strings.ToUpper(state.Network)))
return WalletList(c)
}
func WalletList(c tb.Context) error {
wallets, err := data.GetAllWalletAddress()
if err != nil {
return err
}
var btnList [][]tb.InlineButton
for _, wallet := range wallets {
status := "已启用✅"
if wallet.Status == mdb.TokenStatusDisable {
status = "已禁用🚫"
}
net := wallet.Network
if net == "" {
net = mdb.NetworkTron
}
btnInfo := tb.InlineButton{
Unique: wallet.Address,
Text: fmt.Sprintf("[%s] %s [%s]", net, wallet.Address, status),
Data: strutil.MustString(wallet.ID),
}
bots.Handle(&btnInfo, WalletInfo)
btnList = append(btnList, []tb.InlineButton{btnInfo})
}
addBtn := tb.InlineButton{Text: "添加钱包地址", Unique: "AddWallet"}
bots.Handle(&addBtn, func(c tb.Context) error {
chains, err := getEnabledSupportedNetworks()
if err != nil {
return c.Send("读取支持链失败:" + err.Error())
}
if len(chains) == 0 {
return c.Send("当前没有可用链,请先在后台配置 supported-assets。")
}
rows := make([][]tb.InlineButton, 0, len(chains))
for _, network := range chains {
btn := tb.InlineButton{
Text: strings.ToUpper(network),
Unique: "SelectWalletNetwork",
Data: network,
}
bots.Handle(&btn, SelectWalletNetwork)
rows = append(rows, []tb.InlineButton{btn})
}
return c.EditOrSend(ReplaySelectNetwork, &tb.ReplyMarkup{
InlineKeyboard: rows,
})
})
refreshBtn := tb.InlineButton{Text: "刷新列表", Unique: "WalletRefresh"}
bots.Handle(&refreshBtn, WalletList)
btnList = append(btnList, []tb.InlineButton{addBtn, refreshBtn})
return c.EditOrSend("请选择钱包继续操作", &tb.ReplyMarkup{
InlineKeyboard: btnList,
})
}
func SelectWalletNetwork(c tb.Context) error {
network := strings.ToLower(strings.TrimSpace(c.Data()))
if network == "" {
return c.Send("请选择有效网络")
}
if sender := c.Sender(); sender != nil {
pendingWalletAddressUsers.Store(sender.ID, pendingWalletAddressState{
RequestedAt: time.Now(),
Network: network,
})
}
return c.Send(fmt.Sprintf(ReplayAddWallet, strings.ToUpper(network)), &tb.ReplyMarkup{
ForceReply: true,
})
}
func WalletInfo(c tb.Context) error {
id := mathutil.MustUint(c.Data())
tokenInfo, err := data.GetWalletAddressById(id)
if err != nil {
return c.Send(err.Error())
}
enableBtn := tb.InlineButton{
Text: "启用",
Unique: "enableBtn",
Data: c.Data(),
}
disableBtn := tb.InlineButton{
Text: "禁用",
Unique: "disableBtn",
Data: c.Data(),
}
delBtn := tb.InlineButton{
Text: "删除",
Unique: "delBtn",
Data: c.Data(),
}
backBtn := tb.InlineButton{
Text: "返回",
Unique: "WalletList",
}
bots.Handle(&enableBtn, EnableWallet)
bots.Handle(&disableBtn, DisableWallet)
bots.Handle(&delBtn, DelWallet)
bots.Handle(&backBtn, WalletList)
net := tokenInfo.Network
if net == "" {
net = mdb.NetworkTron
}
detail := fmt.Sprintf("网络:%s\n地址:%s", net, tokenInfo.Address)
return c.EditOrReply(detail, &tb.ReplyMarkup{InlineKeyboard: [][]tb.InlineButton{
{
enableBtn,
disableBtn,
delBtn,
},
{
backBtn,
},
}})
}
func EnableWallet(c tb.Context) error {
id := mathutil.MustUint(c.Data())
if id <= 0 {
return c.Send("请求不合法!")
}
err := data.ChangeWalletAddressStatus(id, mdb.TokenStatusEnable)
if err != nil {
return c.Send(err.Error())
}
return WalletList(c)
}
func DisableWallet(c tb.Context) error {
id := mathutil.MustUint(c.Data())
if id <= 0 {
return c.Send("请求不合法!")
}
err := data.ChangeWalletAddressStatus(id, mdb.TokenStatusDisable)
if err != nil {
return c.Send(err.Error())
}
return WalletList(c)
}
func DelWallet(c tb.Context) error {
id := mathutil.MustUint(c.Data())
if id <= 0 {
return c.Send("请求不合法!")
}
err := data.DeleteWalletAddressById(id)
if err != nil {
return c.Send(err.Error())
}
return WalletList(c)
}
func isWalletAddressPending(userID int64) bool {
if userID <= 0 {
return false
}
value, ok := pendingWalletAddressUsers.Load(userID)
if !ok {
return false
}
state, ok := value.(pendingWalletAddressState)
if !ok || time.Since(state.RequestedAt) > pendingWalletAddressTTL {
pendingWalletAddressUsers.Delete(userID)
return false
}
return true
}
func getPendingWalletAddressState(userID int64) (pendingWalletAddressState, bool) {
v, ok := pendingWalletAddressUsers.Load(userID)
if !ok {
return pendingWalletAddressState{}, false
}
state, ok := v.(pendingWalletAddressState)
return state, ok
}
func getEnabledSupportedNetworks() ([]string, error) {
chains, err := data.ListEnabledChains()
if err != nil {
return nil, err
}
networks := make([]string, 0, len(chains))
for _, ch := range chains {
if n := strings.ToLower(strings.TrimSpace(ch.Network)); n != "" {
networks = append(networks, n)
}
}
sort.Strings(networks)
return networks, nil
}