diff --git a/README.md b/README.md index 9d20850e..f00a2fb0 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ **3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols. > [!IMPORTANT] -> This project is only for personal using, please do not use it for illegal purposes, please do not use it in a production environment. +> This project is only for personal usage, please do not use it for illegal purposes, and please do not use it in a production environment. As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features. diff --git a/web/assets/js/model/inbound.js b/web/assets/js/model/inbound.js index aecedf75..0718b20c 100644 --- a/web/assets/js/model/inbound.js +++ b/web/assets/js/model/inbound.js @@ -729,8 +729,8 @@ class RealityStreamSettings extends XrayCommonClass { constructor( show = false, xver = 0, - target = 'google.com:443', - serverNames = 'google.com,www.google.com', + target = '', + serverNames = '', privateKey = '', minClientVer = '', maxClientVer = '', @@ -740,6 +740,14 @@ class RealityStreamSettings extends XrayCommonClass { settings = new RealityStreamSettings.Settings() ) { super(); + // If target/serverNames are not provided, use random values + if (!target && !serverNames) { + const randomTarget = typeof getRandomRealityTarget !== 'undefined' + ? getRandomRealityTarget() + : { target: 'google.com:443', sni: 'google.com,www.google.com' }; + target = randomTarget.target; + serverNames = randomTarget.sni; + } this.show = show; this.xver = xver; this.target = target; diff --git a/web/assets/js/model/reality_targets.js b/web/assets/js/model/reality_targets.js new file mode 100644 index 00000000..348ff296 --- /dev/null +++ b/web/assets/js/model/reality_targets.js @@ -0,0 +1,86 @@ +// List of popular services for VLESS Reality Target/SNI randomization +const REALITY_TARGETS = [ + // CDN & Cloud Infrastructure + { target: 'www.cloudflare.com:443', sni: 'www.cloudflare.com,cloudflare.com' }, + { target: 'www.microsoft.com:443', sni: 'www.microsoft.com,microsoft.com' }, + { target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' }, + { target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' }, + { target: 'cloud.google.com:443', sni: 'cloud.google.com,www.google.com' }, + { target: 'azure.microsoft.com:443', sni: 'azure.microsoft.com,www.azure.com' }, + { target: 'aws.amazon.com:443', sni: 'aws.amazon.com,amazon.com' }, + { target: 'www.digitalocean.com:443', sni: 'www.digitalocean.com,digitalocean.com' }, + + // Social Media + { target: 'www.facebook.com:443', sni: 'www.facebook.com,facebook.com' }, + { target: 'www.instagram.com:443', sni: 'www.instagram.com,instagram.com' }, + { target: 'www.twitter.com:443', sni: 'www.twitter.com,twitter.com' }, + { target: 'www.linkedin.com:443', sni: 'www.linkedin.com,linkedin.com' }, + { target: 'www.reddit.com:443', sni: 'www.reddit.com,reddit.com' }, + { target: 'www.pinterest.com:443', sni: 'www.pinterest.com,pinterest.com' }, + { target: 'www.tumblr.com:443', sni: 'www.tumblr.com,tumblr.com' }, + + // Video & Streaming + { target: 'www.youtube.com:443', sni: 'www.youtube.com,youtube.com' }, + { target: 'www.netflix.com:443', sni: 'www.netflix.com,netflix.com' }, + { target: 'www.twitch.tv:443', sni: 'www.twitch.tv,twitch.tv' }, + { target: 'vimeo.com:443', sni: 'vimeo.com,www.vimeo.com' }, + { target: 'www.hulu.com:443', sni: 'www.hulu.com,hulu.com' }, + { target: 'www.disneyplus.com:443', sni: 'www.disneyplus.com,disneyplus.com' }, + + // News & Media + { target: 'www.bbc.com:443', sni: 'www.bbc.com,bbc.com' }, + { target: 'www.cnn.com:443', sni: 'www.cnn.com,cnn.com' }, + { target: 'www.nytimes.com:443', sni: 'www.nytimes.com,nytimes.com' }, + { target: 'www.theguardian.com:443', sni: 'www.theguardian.com,theguardian.com' }, + { target: 'www.reuters.com:443', sni: 'www.reuters.com,reuters.com' }, + { target: 'www.bloomberg.com:443', sni: 'www.bloomberg.com,bloomberg.com' }, + + // E-commerce + { target: 'www.ebay.com:443', sni: 'www.ebay.com,ebay.com' }, + { target: 'www.alibaba.com:443', sni: 'www.alibaba.com,alibaba.com' }, + { target: 'www.shopify.com:443', sni: 'www.shopify.com,shopify.com' }, + { target: 'www.walmart.com:443', sni: 'www.walmart.com,walmart.com' }, + { target: 'www.target.com:443', sni: 'www.target.com,target.com' }, + + // Tech Companies + { target: 'www.github.com:443', sni: 'www.github.com,github.com' }, + { target: 'www.stackoverflow.com:443', sni: 'www.stackoverflow.com,stackoverflow.com' }, + { target: 'www.gitlab.com:443', sni: 'www.gitlab.com,gitlab.com' }, + { target: 'www.docker.com:443', sni: 'www.docker.com,docker.com' }, + { target: 'www.nvidia.com:443', sni: 'www.nvidia.com,nvidia.com' }, + { target: 'www.intel.com:443', sni: 'www.intel.com,intel.com' }, + { target: 'www.amd.com:443', sni: 'www.amd.com,amd.com' }, + + // Communication & Productivity + { target: 'www.zoom.us:443', sni: 'www.zoom.us,zoom.us' }, + { target: 'slack.com:443', sni: 'slack.com,www.slack.com' }, + { target: 'www.dropbox.com:443', sni: 'www.dropbox.com,dropbox.com' }, + { target: 'www.notion.so:443', sni: 'www.notion.so,notion.so' }, + { target: 'www.atlassian.com:443', sni: 'www.atlassian.com,atlassian.com' }, + { target: 'www.salesforce.com:443', sni: 'www.salesforce.com,salesforce.com' }, + + // Search & General + { target: 'www.wikipedia.org:443', sni: 'www.wikipedia.org,wikipedia.org' }, + { target: 'www.bing.com:443', sni: 'www.bing.com,bing.com' }, + { target: 'www.yahoo.com:443', sni: 'www.yahoo.com,yahoo.com' }, + { target: 'www.duckduckgo.com:443', sni: 'www.duckduckgo.com,duckduckgo.com' }, + + // Gaming + { target: 'store.steampowered.com:443', sni: 'store.steampowered.com,steampowered.com' }, + { target: 'www.ea.com:443', sni: 'www.ea.com,ea.com' }, + { target: 'www.epicgames.com:443', sni: 'www.epicgames.com,epicgames.com' }, +]; + +/** + * Returns a random Reality target configuration from the predefined list + * @returns {Object} Object with target and sni properties + */ +function getRandomRealityTarget() { + const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length); + const selected = REALITY_TARGETS[randomIndex]; + // Return a copy to avoid reference issues + return { + target: selected.target, + sni: selected.sni + }; +} diff --git a/web/html/form/reality_settings.html b/web/html/form/reality_settings.html index 218ba86d..29170f03 100644 --- a/web/html/form/reality_settings.html +++ b/web/html/form/reality_settings.html @@ -12,10 +12,26 @@ [[ key ]] - + + - + + diff --git a/web/html/inbounds.html b/web/html/inbounds.html index 2ab00f09..8616dce5 100644 --- a/web/html/inbounds.html +++ b/web/html/inbounds.html @@ -602,6 +602,7 @@ {{template "page/body_scripts" .}} + {{template "component/aSidebar" .}} diff --git a/web/html/modals/inbound_modal.html b/web/html/modals/inbound_modal.html index 176f2eee..3c844381 100644 --- a/web/html/modals/inbound_modal.html +++ b/web/html/modals/inbound_modal.html @@ -158,6 +158,13 @@ this.inbound.stream.reality.mldsa65Seed = ''; this.inbound.stream.reality.settings.mldsa65Verify = ''; }, + randomizeRealityTarget() { + if (typeof getRandomRealityTarget !== 'undefined') { + const randomTarget = getRandomRealityTarget(); + this.inbound.stream.reality.target = randomTarget.target; + this.inbound.stream.reality.serverNames = randomTarget.sni; + } + }, async getNewEchCert() { inModal.loading(true); const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni }); diff --git a/web/service/tgbot.go b/web/service/tgbot.go index 0c9d820c..1573b2bf 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -38,7 +38,15 @@ import ( ) var ( - bot *telego.Bot + bot *telego.Bot + + // botCancel stores the function to cancel the context, stopping Long Polling gracefully. + botCancel context.CancelFunc + // tgBotMutex protects concurrent access to botCancel variable + tgBotMutex sync.Mutex + // botWG waits for the OnReceive Long Polling goroutine to finish. + botWG sync.WaitGroup + botHandler *th.BotHandler adminIds []int64 isRunning bool @@ -306,8 +314,13 @@ func (t *Tgbot) SetHostname() { hostname = host } -// Stop stops the Telegram bot and cleans up resources. +// Stop safely stops the Telegram bot's Long Polling operation. +// This method now calls the global StopBot function and cleans up other resources. func (t *Tgbot) Stop() { + // Call the global StopBot function to gracefully shut down Long Polling + StopBot() + + // Stop the bot handler (in case the goroutine hasn't exited yet) if botHandler != nil { botHandler.Stop() } @@ -316,6 +329,27 @@ func (t *Tgbot) Stop() { adminIds = nil } +// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context. +// This is the global function called from main.go's signal handler and t.Stop(). +func StopBot() { + tgBotMutex.Lock() + defer tgBotMutex.Unlock() + + if botCancel != nil { + logger.Info("Sending cancellation signal to Telegram bot...") + + // Calling botCancel() cancels the context passed to UpdatesViaLongPolling, + // which stops the Long Polling operation and closes the updates channel, + // allowing the th.Start() goroutine to exit cleanly. + botCancel() + + botCancel = nil + // Giving the goroutine a small delay to exit cleanly. + botWG.Wait() + logger.Info("Telegram bot successfully stopped.") + } +} + // encodeQuery encodes the query string if it's longer than 64 characters. func (t *Tgbot) encodeQuery(query string) string { // NOTE: we only need to hash for more than 64 chars @@ -345,188 +379,207 @@ func (t *Tgbot) OnReceive() { params := telego.GetUpdatesParams{ Timeout: 30, // Increased timeout to reduce API calls } + // --- GRACEFUL SHUTDOWN FIX: Context creation --- + tgBotMutex.Lock() - updates, _ := bot.UpdatesViaLongPolling(context.Background(), ¶ms) + // Create a context with cancellation and store the cancel function. + var ctx context.Context - botHandler, _ = th.NewBotHandler(bot, updates) + // Check if botCancel is already set (to prevent race condition overwrite and goroutine leak) + if botCancel == nil { + ctx, botCancel = context.WithCancel(context.Background()) + } else { + // If botCancel is already set, use a non-cancellable context for this redundant call. + // This prevents overwriting the active botCancel and causing a goroutine leak from the previous call. + logger.Warning("TgBot OnReceive called concurrently. Using background context for redundant call.") + ctx = context.Background() // <<< ИЗМЕНЕНИЕ + } - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { - delete(userStates, message.Chat.ID) - t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) - return nil - }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) + tgBotMutex.Unlock() - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { - // Use goroutine with worker pool for concurrent command processing - go func() { - messageWorkerPool <- struct{}{} // Acquire worker - defer func() { <-messageWorkerPool }() // Release worker + // Get updates channel using the context. + updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms) + botWG.Go(func() { + botHandler, _ = th.NewBotHandler(bot, updates) + botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { delete(userStates, message.Chat.ID) - t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) - }() - return nil - }, th.AnyCommand()) + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) + return nil + }, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) - botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { - // Use goroutine with worker pool for concurrent callback processing - go func() { - messageWorkerPool <- struct{}{} // Acquire worker - defer func() { <-messageWorkerPool }() // Release worker + botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + // Use goroutine with worker pool for concurrent command processing + go func() { + messageWorkerPool <- struct{}{} // Acquire worker + defer func() { <-messageWorkerPool }() // Release worker - delete(userStates, query.Message.GetChat().ID) - t.answerCallback(&query, checkAdmin(query.From.ID)) - }() - return nil - }, th.AnyCallbackQueryWithMessage()) - - botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { - if userState, exists := userStates[message.Chat.ID]; exists { - switch userState { - case "awaiting_id": - if client_Id == strings.TrimSpace(message.Text) { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) - message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) - t.addClient(message.Chat.ID, message_text) - return nil - } - - client_Id = strings.TrimSpace(message.Text) - if t.isSingleWord(client_Id) { - userStates[message.Chat.ID] = "awaiting_id" - - cancel_btn_markup := tu.InlineKeyboard( - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), - ), - ) - - t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) - } else { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) - message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) - t.addClient(message.Chat.ID, message_text) - } - case "awaiting_password_tr": - if client_TrPassword == strings.TrimSpace(message.Text) { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - return nil - } - - client_TrPassword = strings.TrimSpace(message.Text) - if t.isSingleWord(client_TrPassword) { - userStates[message.Chat.ID] = "awaiting_password_tr" - - cancel_btn_markup := tu.InlineKeyboard( - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), - ), - ) - - t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) - } else { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) - message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) - t.addClient(message.Chat.ID, message_text) - } - case "awaiting_password_sh": - if client_ShPassword == strings.TrimSpace(message.Text) { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - return nil - } - - client_ShPassword = strings.TrimSpace(message.Text) - if t.isSingleWord(client_ShPassword) { - userStates[message.Chat.ID] = "awaiting_password_sh" - - cancel_btn_markup := tu.InlineKeyboard( - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), - ), - ) - - t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) - } else { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) - message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) - t.addClient(message.Chat.ID, message_text) - } - case "awaiting_email": - if client_Email == strings.TrimSpace(message.Text) { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - return nil - } - - client_Email = strings.TrimSpace(message.Text) - if t.isSingleWord(client_Email) { - userStates[message.Chat.ID] = "awaiting_email" - - cancel_btn_markup := tu.InlineKeyboard( - tu.InlineKeyboardRow( - tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), - ), - ) - - t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) - } else { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) - message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) - t.addClient(message.Chat.ID, message_text) - } - case "awaiting_comment": - if client_Comment == strings.TrimSpace(message.Text) { - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) - delete(userStates, message.Chat.ID) - return nil - } - - client_Comment = strings.TrimSpace(message.Text) - t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove()) delete(userStates, message.Chat.ID) - inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) - message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) - t.addClient(message.Chat.ID, message_text) - } + t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) + }() + return nil + }, th.AnyCommand()) - } else { - if message.UsersShared != nil { - if checkAdmin(message.From.ID) { - for _, sharedUser := range message.UsersShared.Users { - userID := sharedUser.UserID - needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID) - if needRestart { - t.xrayService.SetToNeedRestart() - } - output := "" - if err != nil { - output += t.I18nBot("tgbot.messages.selectUserFailed") - } else { - output += t.I18nBot("tgbot.messages.userSaved") - } - t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove()) + botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { + // Use goroutine with worker pool for concurrent callback processing + go func() { + messageWorkerPool <- struct{}{} // Acquire worker + defer func() { <-messageWorkerPool }() // Release worker + + delete(userStates, query.Message.GetChat().ID) + t.answerCallback(&query, checkAdmin(query.From.ID)) + }() + return nil + }, th.AnyCallbackQueryWithMessage()) + + botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { + if userState, exists := userStates[message.Chat.ID]; exists { + switch userState { + case "awaiting_id": + if client_Id == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + return nil + } + + client_Id = strings.TrimSpace(message.Text) + if t.isSingleWord(client_Id) { + userStates[message.Chat.ID] = "awaiting_id" + + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) + } else { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + case "awaiting_password_tr": + if client_TrPassword == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + return nil + } + + client_TrPassword = strings.TrimSpace(message.Text) + if t.isSingleWord(client_TrPassword) { + userStates[message.Chat.ID] = "awaiting_password_tr" + + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) + } else { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + case "awaiting_password_sh": + if client_ShPassword == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + return nil + } + + client_ShPassword = strings.TrimSpace(message.Text) + if t.isSingleWord(client_ShPassword) { + userStates[message.Chat.ID] = "awaiting_password_sh" + + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) + } else { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + case "awaiting_email": + if client_Email == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + return nil + } + + client_Email = strings.TrimSpace(message.Text) + if t.isSingleWord(client_Email) { + userStates[message.Chat.ID] = "awaiting_email" + + cancel_btn_markup := tu.InlineKeyboard( + tu.InlineKeyboardRow( + tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"), + ), + ) + + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup) + } else { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + case "awaiting_comment": + if client_Comment == strings.TrimSpace(message.Text) { + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + return nil + } + + client_Comment = strings.TrimSpace(message.Text) + t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove()) + delete(userStates, message.Chat.ID) + inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) + message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) + t.addClient(message.Chat.ID, message_text) + } + + } else { + if message.UsersShared != nil { + if checkAdmin(message.From.ID) { + for _, sharedUser := range message.UsersShared.Users { + userID := sharedUser.UserID + needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID) + if needRestart { + t.xrayService.SetToNeedRestart() + } + output := "" + if err != nil { + output += t.I18nBot("tgbot.messages.selectUserFailed") + } else { + output += t.I18nBot("tgbot.messages.userSaved") + } + t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove()) + } + } else { + t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove()) } - } else { - t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove()) } } - } - return nil - }, th.AnyMessage()) + return nil + }, th.AnyMessage()) - botHandler.Start() + botHandler.Start() + }) } // answerCommand processes incoming command messages from Telegram users. diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 38e52086..4db4a5ce 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -32,7 +32,7 @@ "copySuccess" = "Скопировано" "sure" = "Да" "encryption" = "Шифрование" -"useIPv4ForHost" = "Использовать IPv4 для хоста" +"useIPv4ForHost" = "Использовать IPv4 для подключения к хосту" "transmission" = "Транспорт" "host" = "Хост" "path" = "Путь" @@ -46,8 +46,8 @@ "online" = "Онлайн" "domainName" = "Домен" "monitor" = "Мониторинг IP" -"certificate" = "SSL сертификат" -"fail" = "Ошибка" +"certificate" = "SSL-сертификат" +"fail" = "Сбой" "comment" = "Комментарий" "success" = "Успешно" "lastOnline" = "Был(а) в сети" @@ -55,17 +55,17 @@ "install" = "Установка" "clients" = "Клиенты" "usage" = "Использование" -"twoFactorCode" = "Код" +"twoFactorCode" = "Код 2FA" "remained" = "Остаток" "security" = "Безопасность" "secAlertTitle" = "Предупреждение системы безопасности" -"secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения" -"secAlertConf" = "Некоторые настройки уязвимы для атак. Чтобы в будущем не было проблем, нужно усилить защиту." -"secAlertSSL" = "Ваше подключение к панели не защищено. Установите SSL сертификат для защиты данных." -"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите случайный или просто другой порт." -"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Сделайте адрес сложным." -"secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес." -"secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес." +"secAlertSsl" = "Соединение не защищено. Не вводите конфиденциальные данные до установки SSL-сертификата." +"secAlertConf" = "Некоторые настройки уязвимы. Рекомендуется усилить защиту для предотвращения атак." +"secAlertSSL" = "Подключение к панели не защищено. Установите SSL-сертификат для защиты данных." +"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите нестандартный или случайный порт." +"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Настройте уникальный и сложный URI." +"secAlertSubURI" = "URI подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес." +"secAlertSubJsonURI" = "URI JSON-подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес." "emptyDnsDesc" = "Нет добавленных DNS-серверов." "emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов." "emptyBalancersDesc" = "Нет добавленных балансировщиков." @@ -86,15 +86,15 @@ "individualLinks" = "Индивидуальные ссылки" "active" = "Активна" "inactive" = "Неактивна" -"unlimited" = "Безлимит" -"noExpiry" = "Без срока" +"unlimited" = "Неограниченно" +"noExpiry" = "Бессрочно" [menu] "theme" = "Тема" "dark" = "Темная" "ultraDark" = "Очень темная" "dashboard" = "Дашборд" -"inbounds" = "Инбаунды" +"inbounds" = "Подключения" "settings" = "Настройки" "xray" = "Настройки Xray" "logout" = "Выход" @@ -110,7 +110,7 @@ "emptyUsername" = "Введите имя пользователя" "emptyPassword" = "Введите пароль" "wrongUsernameOrPassword" = "Неверные данные учетной записи." -"successLogin" = "Вы успешно вошли в аккаунт" +"successLogin" = "Вход выполнен успешно" [pages.index] "title" = "Дашборд" @@ -125,7 +125,7 @@ "stopXray" = "Остановить" "restartXray" = "Перезапустить" "xraySwitch" = "Выбор версии" -"xraySwitchClick" = "Выберите желаемую версию" +"xraySwitchClick" = "Выберите нужную версию" "xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки" "xrayStatusUnknown" = "Неизвестно" "xrayStatusRunning" = "Запущен" @@ -137,7 +137,7 @@ "systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут" "connectionCount" = "Количество соединений" "ipAddresses" = "IP-адреса сервера" -"toggleIpVisibility" = "Переключить видимость IP-адресов сервера" +"toggleIpVisibility" = "Скрыть или показать IP-адреса сервера" "overallSpeed" = "Общая скорость передачи трафика" "upload" = "Отправка" "download" = "Загрузка" @@ -171,10 +171,10 @@ [pages.inbounds] "allTimeTraffic" = "Общий трафик" "allTimeTrafficUsage" = "Общее использование за все время" -"title" = "Инбаунды" -"totalDownUp" = "Объем отправленного/полученного трафика" +"title" = "Подключения" +"totalDownUp" = "Отправлено/получено" "totalUsage" = "Всего трафика" -"inboundCount" = "Всего инбаундов" +"inboundCount" = "Всего подключений" "operate" = "Меню" "enable" = "Включить" "remark" = "Примечание" @@ -188,13 +188,13 @@ "createdAt" = "Создано" "updatedAt" = "Обновлено" "resetTraffic" = "Сброс трафика" -"addInbound" = "Создать инбаунд" +"addInbound" = "Создать подключение" "generalActions" = "Общие действия" "autoRefresh" = "Автообновление" "autoRefreshInterval" = "Интервал" -"modifyInbound" = "Изменить инбаунд" -"deleteInbound" = "Удалить инбаунд" -"deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?" +"modifyInbound" = "Изменить подключение" +"deleteInbound" = "Удалить подключение" +"deleteInboundContent" = "Вы уверены, что хотите удалить подключение?" "deleteClient" = "Удалить клиента" "deleteClientContent" = "Вы уверены, что хотите удалить клиента?" "resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?" @@ -217,11 +217,11 @@ "export" = "Экспорт ссылок" "clone" = "Клонировать" "cloneInbound" = "Клонировать" -"cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания" +"cloneInboundContent" = "Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания" "cloneInboundOk" = "Клонировано" -"resetAllTraffic" = "Сброс трафика всех инбаундов" -"resetAllTrafficTitle" = "Сброс трафика всех инбаундов" -"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?" +"resetAllTraffic" = "Сброс трафика всех подключений" +"resetAllTrafficTitle" = "Сброс трафика всех подключений" +"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?" "resetInboundClientTraffics" = "Сброс трафика клиента" "resetInboundClientTrafficTitle" = "Сброс трафика клиентов" "resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?" @@ -234,7 +234,7 @@ "email" = "Email" "emailDesc" = "Пожалуйста, укажите уникальный Email" "IPLimit" = "Лимит по количеству IP" -"IPLimitDesc" = "Ограничение количества одновременных подключений с разных IP(0 – отключить)" +"IPLimitDesc" = "Ограничение числа одновременных подключений с разных IP (0 – отключить)" "IPLimitlog" = "Лог IP-адресов" "IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)" "IPLimitlogclear" = "Очистить лог" @@ -243,19 +243,19 @@ "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'" "info" = "Информация" "same" = "Тот же" -"inboundData" = "Данные инбаундов" -"exportInbound" = "Экспорт инбаундов" +"inboundData" = "Данные подключений" +"exportInbound" = "Экспорт подключений" "import" = "Импортировать" -"importInbound" = "Импорт инбаундов" +"importInbound" = "Импорт подключений" "periodicTrafficResetTitle" = "Сброс трафика" "periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы" "lastReset" = "Последний сброс" [pages.client] -"add" = "Создать клиента" +"add" = "Добавить клиента" "edit" = "Редактировать клиента" "submitAdd" = "Добавить" -"submitEdit" = "Сохранить" +"submitEdit" = "Сохранить изменения" "clientCount" = "Количество клиентов" "bulk" = "Добавить несколько" "method" = "Метод" @@ -279,13 +279,13 @@ "obtain" = "Получить" "updateSuccess" = "Обновление прошло успешно" "logCleanSuccess" = "Лог был очищен" -"inboundsUpdateSuccess" = "Инбаунды успешно обновлены" -"inboundUpdateSuccess" = "Инбаунд успешно обновлено" -"inboundCreateSuccess" = "Инбаунд успешно создано" -"inboundDeleteSuccess" = "Инбаунд успешно удалено" -"inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)" -"inboundClientDeleteSuccess" = "Клиент инбаунда удалён" -"inboundClientUpdateSuccess" = "Клиент инбаунда обновлён" +"inboundsUpdateSuccess" = "Подключения успешно обновлены" +"inboundUpdateSuccess" = "Подключение успешно обновлено" +"inboundCreateSuccess" = "Подключение успешно создано" +"inboundDeleteSuccess" = "Подключение успешно удалено" +"inboundClientAddSuccess" = "Клиент(ы) подключения добавлен(ы)" +"inboundClientDeleteSuccess" = "Клиент подключения удалён" +"inboundClientUpdateSuccess" = "Клиент подключения обновлён" "delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены" "resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен" "resetAllTrafficSuccess" = "Весь трафик сброшен" @@ -313,7 +313,7 @@ [pages.settings] "title" = "Настройки" "save" = "Сохранить" -"infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу." +"infoDesc" = "Сохраните изменения и перезапустите панель для их применения." "restartPanel" = "Перезапуск панели" "restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера" "restartPanelSuccess" = "Панель успешно перезапущена" @@ -321,11 +321,11 @@ "resetDefaultConfig" = "Восстановить настройки по умолчанию" "panelSettings" = "Панель" "securitySettings" = "Учетная запись" -"TGBotSettings" = "Telegram" +"TGBotSettings" = "Telegram-Бот" "panelListeningIP" = "IP-адрес для управления панелью" "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" "panelListeningDomain" = "Домен панели" -"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы подключаться с любых доменов и IP-адресов" +"panelListeningDomainDesc" = "Оставьте пустым для подключения с любых доменов и IP." "panelPort" = "Порт панели" "panelPortDesc" = "Порт, на котором работает панель" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" @@ -335,11 +335,11 @@ "panelUrlPath" = "Корневой путь URL адреса панели" "panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'" "pageSize" = "Размер нумерации страниц" -"pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить" +"pageSizeDesc" = "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить" "remarkModel" = "Модель примечания и символ разделения" -"datepicker" = "Выбор даты" +"datepicker" = "Тип календаря" "datepickerPlaceholder" = "Выберите дату" -"datepickerDescription" = "Запланированные задачи будут выполняться в выбранное время" +"datepickerDescription" = "Запланированные задачи будут выполняться в соответствии с этим календарем." "sampleRemark" = "Пример примечания" "oldUsername" = "Текущий логин" "currentPassword" = "Текущий пароль" @@ -349,7 +349,7 @@ "telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота" "telegramToken" = "Токен Telegram бота" "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather" -"telegramProxy" = "Прокси Socks5" +"telegramProxy" = "Прокси-сервер Socks5" "telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству." "telegramAPIServer" = "API-сервер Telegram" "telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию." @@ -454,11 +454,11 @@ "RoutingStrategy" = "Настройка маршрутизации доменов" "RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS" "Torrent" = "Заблокировать BitTorrent" -"Inbounds" = "Инбаунды" +"Inbounds" = "Входящие подключения" "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов" -"Outbounds" = "Аутбаунды" +"Outbounds" = "Исходящие подключения" "Balancers" = "Балансировщик" -"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера" +"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера" "Routings" = "Маршрутизация" "RoutingsDesc" = "Важен приоритет каждого правила!" "completeTemplate" = "Все" @@ -489,8 +489,8 @@ "down" = "Опустить вниз" "source" = "Источник" "dest" = "Пункт назначения" -"inbound" = "Инбаунд" -"outbound" = "Аутбаунд" +"inbound" = "Входящее подключение" +"outbound" = "Исходящее подключение" "balancer" = "Балансировщик" "info" = "Информация" "add" = "Создать правило" @@ -498,9 +498,9 @@ "useComma" = "Элементы, разделённые запятыми" [pages.xray.outbound] -"addOutbound" = "Создать аутбаунд" +"addOutbound" = "Создать исходящее подключение" "addReverse" = "Создать реверс-прокси" -"editOutbound" = "Изменить аутбаунд" +"editOutbound" = "Изменить исходящее подключение" "editReverse" = "Редактировать реверс-прокси" "tag" = "Тег" "tagDesc" = "Уникальный тег" @@ -514,7 +514,7 @@ "intercon" = "Соединение" "settings" = "Настройки" "accountInfo" = "Информация об учетной записи" -"outboundStatus" = "Статус аутбаунда" +"outboundStatus" = "Статус исходящего подключения" "sendThrough" = "Отправить через" [pages.xray.balancer] @@ -590,8 +590,8 @@ "modifyUser" = "Вы успешно изменили учетные данные администратора." "originalUserPassIncorrect" = "Неверное имя пользователя или пароль" "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" -"getOutboundTrafficError" = "Ошибка получения трафика аутбаунда" -"resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда" +"getOutboundTrafficError" = "Ошибка получения трафика исходящего подключения" +"resetOutboundTrafficError" = "Ошибка сброса трафика исходящего подключения" [tgbot] "keyboardClosed" = "❌ Клавиатура закрыта." @@ -599,7 +599,7 @@ "noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду." "wentWrong" = "❌ Что-то пошло не так..." "noIpRecord" = "❗ Нет записей об IP-адресе." -"noInbounds" = "❗ У вас не настроено ни одного инбаунда." +"noInbounds" = "❗ У вас не настроено ни одного входящего подключения." "unlimited" = "♾ Безлимит" "add" = "Добавить" "month" = "Месяц" @@ -609,7 +609,7 @@ "hours" = "Часов" "minutes" = "Минуты" "unknown" = "Неизвестно" -"inbounds" = "Инбаунды" +"inbounds" = "Входящие подключения" "clients" = "Клиенты" "offline" = "🔴 Офлайн" "online" = "🟢 Онлайн" @@ -623,7 +623,7 @@ "status" = "✅ Бот функционирует нормально." "usage" = "❗ Пожалуйста, укажите email для поиска." "getID" = "🆔 Ваш User ID: {{ .ID }}" -"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n/restart\r\n\r\n🔎 Для поиска клиента по email:\r\n/usage [Email]\r\n\r\n📊 Для поиска инбаундов (со статистикой клиентов):\r\n/inbound [имя подключения]\r\n\r\n🆔 Ваш Telegram User ID:\r\n/id" +"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n/restart\r\n\r\n🔎 Для поиска клиента по email:\r\n/usage [Email]\r\n\r\n📊 Для поиска входящих подключений (со статистикой клиентов):\r\n/inbound [имя подключения]\r\n\r\n🆔 Ваш Telegram User ID:\r\n/id" "helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n/usage [Email]\r\n\r\n🆔 Ваш Telegram User ID:\r\n/id" "restartUsage" = "\r\n\r\n/restart" "restartSuccess" = "✅ Ядро Xray успешно перезапущено." @@ -659,7 +659,7 @@ "username" = "👤 Имя пользователя: {{ .Username }}\r\n" "password" = "👤 Пароль: {{ .Password }}\r\n" "time" = "⏰ Время: {{ .Time }}\r\n" -"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n" +"inbound" = "📍 Входящее подключение: {{ .Remark }}\r\n" "port" = "🔌 Порт: {{ .Port }}\r\n" "expire" = "📅 Дата окончания: {{ .Time }}\r\n" "expireIn" = "📅 Окончание через: {{ .Time }}\r\n" @@ -688,12 +688,12 @@ "pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль." "email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email." "comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий." -"inbound_client_data_id" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!" -"inbound_client_data_pass" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!" +"inbound_client_data_id" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!" +"inbound_client_data_pass" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!" "cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄" -"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}" -"using_default_value" = "Используется значение по умолчанию👌" -"incorrect_input" ="Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫" +"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}" +"using_default_value" = "Используется значение по умолчанию👌" +"incorrect_input" = "Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫" "AreYouSure" = "Вы уверены? 🤔" "SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно" "FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠️ Ошибка: [ {{ .ErrorMessage }} ]" @@ -710,7 +710,7 @@ "confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?" "dbBackup" = "📂 Бэкап БД" "serverUsage" = "💻 Состояние сервера" -"getInbounds" = "🔌 Инбаунды" +"getInbounds" = "🔌 Входящие подключения" "depleteSoon" = "⚠️ Скоро конец" "clientUsage" = "Статистика клиента" "onlines" = "🟢 Онлайн" @@ -734,7 +734,7 @@ "allClients" = "👥 Все клиенты" "addClient" = "➕ Новый клиент" "submitDisable" = "Добавить отключенным ☑️" -"submitEnable" = "Добавить включенныи ✅" +"submitEnable" = "Добавить включенным ✅" "use_default" = "🏷️ Использовать по умолчанию" "change_id" = "⚙️🔑 ID" "change_password" = "⚙️🔑 Пароль" @@ -746,7 +746,7 @@ [tgbot.answers] "successfulOperation" = "✅ Успешно!" "errorOperation" = "❗ Ошибка в операции." -"getInboundsFailed" = "❌ Не удалось получить инбаунды." +"getInboundsFailed" = "❌ Не удалось получить входящие подключения." "getClientsFailed" = "❌ Не удалось получить клиентов." "canceled" = "❌ {{ .Email }}: Операция отменена." "clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен." @@ -763,5 +763,5 @@ "enableSuccess" = "✅ {{ .Email }}: Включено успешно." "disableSuccess" = "✅ {{ .Email }}: Отключено успешно." "askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: {{ .TgUserID }}" -"chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}" -"chooseInbound" = "Выберите инбаунд" +"chooseClient" = "Выберите клиента для входящего подключения {{ .Inbound }}" +"chooseInbound" = "Выберите входящее подключение"