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 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()) } } }