mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-03 19:55:04 +03:00
83ea59fb69
The v3.11.0 bot rebuild left the admin's client card without the subscription link, individual links and QR code, so an admin could not hand a config to a client who cannot reach Telegram yet. The card now carries Links and QR code buttons that open the existing screens. Closes #6984.
153 lines
6.0 KiB
Go
153 lines
6.0 KiB
Go
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, delivery ...screenDelivery) {
|
||
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)...), delivery...)
|
||
}
|
||
|
||
// 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)),
|
||
),
|
||
// An invite link needs the client to reach Telegram; a subscription URL
|
||
// and a QR code do not, so they are the admin's way to hand one over.
|
||
tu.InlineKeyboardRow(
|
||
t.btn("tgbot.buttons.links", t.encodeQuery("client_sub_links "+email)),
|
||
t.btn("tgbot.buttons.qrCode", t.encodeQuery("client_qr_links "+email)),
|
||
),
|
||
tu.InlineKeyboardRow(t.btn("tgbot.buttons.toggle", t.encodeQuery("toggle_enable "+email))),
|
||
t.backRow(),
|
||
}
|
||
}
|