mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
15fee2a035
The bot answered every action with a new message, so a chat filled with stale menus whose buttons still acted, and long client or inbound lists were paged through dozens of buttons. Each chat now has one live screen that is edited in place: the menu is grouped into Server, Clients, Traffic and Maintenance, lists open through inline search, the add-client flow starts from a draft card, QR codes arrive as one labelled sheet, and repeated notices collapse into one with a counter. A client's /start and /usage show only that client's own links and usage, groups are refused, and the settings page warns when the bot's inline mode is off. Admin topics, the indexed Telegram lookup and every locale, Slovak included, carry over.
150 lines
5.5 KiB
Go
150 lines
5.5 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/util/common"
|
|
|
|
"github.com/mymmrac/telego"
|
|
tu "github.com/mymmrac/telego/telegoutil"
|
|
)
|
|
|
|
// getInboundUsages retrieves and formats inbound usage information.
|
|
func (t *Tgbot) getInboundUsages() string {
|
|
var info strings.Builder
|
|
inbounds, err := t.inboundService.GetAllInbounds()
|
|
if err != nil {
|
|
logger.Warning("GetAllInbounds run failed:", err)
|
|
info.WriteString(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
|
return info.String()
|
|
}
|
|
for _, inbound := range inbounds {
|
|
info.WriteString(t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark))
|
|
info.WriteString(t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port)))
|
|
info.WriteString(t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down)))
|
|
|
|
clients, listErr := t.clientService.ListForInbound(nil, inbound.Id)
|
|
if listErr == nil {
|
|
fmt.Fprintf(&info, "👥 Clients: %d\r\n", len(clients))
|
|
}
|
|
|
|
if inbound.ExpiryTime == 0 {
|
|
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited")))
|
|
} else {
|
|
info.WriteString(t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05")))
|
|
}
|
|
info.WriteString("\r\n")
|
|
}
|
|
return info.String()
|
|
}
|
|
|
|
// addClientExcludedProtocols are the protocols with no per-client model: Tunnel
|
|
// has no clients, Mixed/HTTP authenticate at the inbound level, not per-client.
|
|
var addClientExcludedProtocols = map[model.Protocol]bool{
|
|
model.Tunnel: true,
|
|
model.Mixed: true,
|
|
model.HTTP: true,
|
|
}
|
|
|
|
// getInboundsAttachPicker builds a toggle picker over multi-client inbounds
|
|
// for the "attach more inbounds to the new client" step. Each row shows the
|
|
// current selection state for the inbound; tapping fires
|
|
// add_client_toggle_attach <id> which flips it and re-renders. A final
|
|
// "Done" button (add_client_attach_done) returns to the field-edit screen.
|
|
func (t *Tgbot) getInboundsAttachPicker(draft *clientDraft) (*telego.InlineKeyboardMarkup, error) {
|
|
inbounds, err := t.inboundService.GetAllInbounds()
|
|
if err != nil {
|
|
logger.Warning("GetAllInbounds run failed:", err)
|
|
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
|
}
|
|
if len(inbounds) == 0 {
|
|
return nil, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
|
|
}
|
|
selected := make(map[int]bool, len(draft.receiverInboundIDs))
|
|
for _, id := range draft.receiverInboundIDs {
|
|
selected[id] = true
|
|
}
|
|
var buttons []telego.InlineKeyboardButton
|
|
for _, ib := range inbounds {
|
|
if addClientExcludedProtocols[ib.Protocol] {
|
|
continue
|
|
}
|
|
mark := "☐"
|
|
if selected[ib.Id] {
|
|
mark = "✅"
|
|
}
|
|
label := fmt.Sprintf("%s %s (%s)", mark, ib.Remark, ib.Protocol)
|
|
callback := t.encodeQuery(fmt.Sprintf("add_client_toggle_attach %d", ib.Id))
|
|
buttons = append(buttons, tu.InlineKeyboardButton(label).WithCallbackData(callback))
|
|
}
|
|
cols := 1
|
|
if len(buttons) >= 6 {
|
|
cols = 2
|
|
}
|
|
rows := tu.InlineKeyboardCols(cols, buttons...)
|
|
rows = append(rows, tu.InlineKeyboardRow(
|
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.attachDone")).
|
|
WithCallbackData(t.encodeQuery("add_client_attach_done")),
|
|
))
|
|
return tu.InlineKeyboardGrid(rows), nil
|
|
}
|
|
|
|
// getInboundClientsFor builds the client list for the caller's inbound row: a
|
|
// second DB read would reopen the stale-row window the caller's guard closed.
|
|
func (t *Tgbot) getInboundClientsFor(inbound *model.Inbound, action string) (*telego.InlineKeyboardMarkup, error) {
|
|
clients, err := t.inboundService.GetClients(inbound)
|
|
if err != nil || len(clients) == 0 {
|
|
return nil, errors.New(t.I18nBot("tgbot.answers.getClientsFailed"))
|
|
}
|
|
rows := make([][]telego.InlineKeyboardButton, 0, len(clients))
|
|
for _, client := range clients {
|
|
rows = append(rows, tu.InlineKeyboardRow(tu.InlineKeyboardButton(client.Email).
|
|
WithCallbackData(t.encodeQuery(action+" "+client.Email))))
|
|
}
|
|
return tu.InlineKeyboard(rows...), nil
|
|
}
|
|
|
|
// searchInbound searches for inbounds by remark and sends the results.
|
|
func (t *Tgbot) searchInbound(chatId int64, remark string) {
|
|
inbounds, err := t.inboundService.SearchInbounds(remark)
|
|
if err != nil {
|
|
logger.Warning(err)
|
|
msg := t.I18nBot("tgbot.wentWrong")
|
|
t.SendMsgToTgbot(chatId, msg)
|
|
return
|
|
}
|
|
if len(inbounds) == 0 {
|
|
msg := t.I18nBot("tgbot.noInbounds")
|
|
t.SendMsgToTgbot(chatId, msg)
|
|
return
|
|
}
|
|
|
|
for _, inbound := range inbounds {
|
|
info := ""
|
|
info += t.I18nBot("tgbot.messages.inbound", "Remark=="+inbound.Remark)
|
|
info += t.I18nBot("tgbot.messages.port", "Port=="+strconv.Itoa(inbound.Port))
|
|
info += t.I18nBot("tgbot.messages.traffic", "Total=="+common.FormatTraffic((inbound.Up+inbound.Down)), "Upload=="+common.FormatTraffic(inbound.Up), "Download=="+common.FormatTraffic(inbound.Down))
|
|
|
|
if inbound.ExpiryTime == 0 {
|
|
info += t.I18nBot("tgbot.messages.expire", "Time=="+t.I18nBot("tgbot.unlimited"))
|
|
} else {
|
|
info += t.I18nBot("tgbot.messages.expire", "Time=="+time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
|
}
|
|
t.SendMsgToTgbot(chatId, info)
|
|
|
|
if len(inbound.ClientStats) > 0 {
|
|
var output strings.Builder
|
|
for _, traffic := range inbound.ClientStats {
|
|
output.WriteString(t.clientInfoMsg(&traffic, true, true, true, true, true, true))
|
|
}
|
|
t.SendMsgToTgbot(chatId, output.String())
|
|
}
|
|
}
|
|
}
|