Files
3x-ui/internal/web/service/tgbot/screen_legacy.go
T
DIMFLIX 15fee2a035 feat(tgbot): rebuild the Telegram bot around editable screens and inline lists (#6760)
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.
2026-10-24 12:39:56 +02:00

147 lines
5.7 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 tgbot
import (
"github.com/mhsanaei/3x-ui/v3/internal/logger"
"github.com/mymmrac/telego"
tu "github.com/mymmrac/telego/telegoutil"
)
// Bridges for the screens still served by the legacy views. Each one is a
// screen now, so the router's callback cases only decide which view to draw.
// clearScreen drops the tracked screen and the user's own message: the pilot
// tap that closes the reply keyboard must leave nothing behind.
func (t *Tgbot) clearScreen(message telego.Message) {
t.deleteIncoming(&message)
if sc, ok := t.screens().get(message.Chat.ID); ok && sc.msgID != 0 {
t.deleteOwnMessage(message.Chat.ID, sc.msgID)
}
t.screens().clear(message.Chat.ID)
}
// hideMessage deletes the very message whose button was tapped. The id comes
// from the callback, so nothing has to be remembered to make it work.
func (t *Tgbot) hideMessage(query *telego.CallbackQuery) {
if query.Message == nil {
return
}
chat := query.Message.GetChat()
t.deleteOwnMessage(chat.ID, query.Message.GetMessageID())
// A hidden notice must not stay the chat's screen either, and the next event
// of its kind has to start a fresh card rather than edit a deleted one.
if sc, ok := t.screens().get(chat.ID); ok && sc.msgID == query.Message.GetMessageID() {
t.screens().clear(chat.ID)
}
t.dropNoticeFor(chat.ID, query.Message.GetMessageID())
t.answerSilent(query.ID)
}
// turnPage moves the stored screen one body-page back or forward.
func (t *Tgbot) turnPage(query *telego.CallbackQuery, delta int) {
chatID := query.Message.GetChat().ID
sc, ok := t.screens().get(chatID)
if !ok {
t.answerSilent(query.ID)
return
}
t.showPage(chatID, sc.page+delta)
t.answerSilent(query.ID)
}
// resetAllConfirm is the confirmation screen for the panel-wide traffic reset.
func (t *Tgbot) resetAllConfirm() *screen {
// The forward action and the way out must be different rows: sharing one
// row made "Cancel" read as part of the confirmation.
return t.newScreen("main", t.I18nBot("tgbot.messages.AreYouSure"),
tu.InlineKeyboardRow(t.btn("tgbot.buttons.confirmResetTraffic", "reset_all_traffics_c")),
tu.InlineKeyboardRow(t.btn("tgbot.buttons.back", cbHome)),
)
}
// screenInbounds summarises the inbounds; the browsable, searchable list is the
// inline query, so the screen carries only the summary and the way back.
func (t *Tgbot) screenInbounds(chatID, userID int64) {
_ = userID
body := t.getInboundUsages()
body += t.I18nBot("tgbot.messages.inlineHint")
rows := [][]telego.InlineKeyboardButton{
t.launchRow(scopeQuery(inlineScopeInbounds, ""), "tgbot.buttons.searchInbounds"),
t.backRow(),
}
t.renderScreen(chatID, t.newScreen("inbounds", body, rows...))
}
// browseInboundsScreen is the pick-an-inbound step: the list itself is the
// inline browser, so the screen carries the hint and the search launcher.
func (t *Tgbot) browseInboundsScreen(chatID, userID int64) {
_ = userID
rows := [][]telego.InlineKeyboardButton{
t.launchRow(scopeQuery(inlineScopeInbounds, ""), "tgbot.buttons.searchInbounds"),
t.backRow(),
}
t.renderScreen(chatID, t.newScreen("inbounds", t.I18nBot("tgbot.answers.chooseInbound"), rows...))
}
// screenAddClientStart opens the add-client wizard on a fresh draft: filling the
// draft is the first step, and inbounds are picked on "➕ Attach inbound".
func (t *Tgbot) screenAddClientStart(chatID, userID int64) {
actor := chatUser{chatID: chatID, userID: userID}
addClientDrafts.reset(actor)
draft := addClientDrafts.forActor(actor)
draft.Lock()
defer draft.Unlock()
t.resetDraft(draft)
t.addClient(chatID, draft, t.BuildClientDraftMessage(draft))
}
// restartXrayFromScreen restarts Xray and reports the outcome as a toast, so
// the menu screen itself stays put.
func (t *Tgbot) restartXrayFromScreen(query *telego.CallbackQuery) {
if !t.xrayService.IsXrayRunning() {
t.sendCallbackAnswerTgBot(query.ID, t.I18nBot("tgbot.commands.xrayNotRunning"))
return
}
if err := t.xrayService.RestartXray(true); err != nil {
logger.Warning("Xray restart failed:", err)
t.sendCallbackAnswerTgBot(query.ID, t.I18nBot("tgbot.commands.restartFailed", "Error=="+err.Error()))
return
}
t.sendCallbackAnswerTgBot(query.ID, t.I18nBot("tgbot.commands.restartSuccess"))
}
// searchClientScreen renders the client card as a screen; every per-email
// callback that used to post a message lands here.
func (t *Tgbot) searchClientScreen(chatID int64, email string) {
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil || traffic == nil {
t.sendNotice(chatID, t.I18nBot("tgbot.noResult"))
return
}
body := t.clientInfoMsg(traffic, true, true, true, true, true, true)
t.renderScreen(chatID, t.newScreen("client", body, t.clientRows(email)...))
}
// clientRows is the client card's keyboard, in one place so every entry point
// draws an identical card.
func (t *Tgbot) clientRows(email string) [][]telego.InlineKeyboardButton {
return [][]telego.InlineKeyboardButton{
tu.InlineKeyboardRow(t.btn("tgbot.buttons.refresh", t.encodeQuery("client_refresh "+email))),
tu.InlineKeyboardRow(
t.btn("tgbot.buttons.resetTraffic", t.encodeQuery("reset_traffic "+email)),
t.btn("tgbot.buttons.limitTraffic", t.encodeQuery("limit_traffic "+email)),
),
tu.InlineKeyboardRow(t.btn("tgbot.buttons.resetExpire", t.encodeQuery("reset_exp "+email))),
tu.InlineKeyboardRow(
t.btn("tgbot.buttons.ipLog", t.encodeQuery("ip_log "+email)),
t.btn("tgbot.buttons.ipLimit", t.encodeQuery("ip_limit "+email)),
),
tu.InlineKeyboardRow(
t.btn("tgbot.buttons.setTGUser", t.encodeQuery("tg_user "+email)),
t.btn("tgbot.buttons.inviteLink", t.encodeQuery("client_invite_link "+email)),
),
tu.InlineKeyboardRow(t.btn("tgbot.buttons.toggle", t.encodeQuery("toggle_enable "+email))),
t.backRow(),
}
}