5 커밋 713a7328f6 ... 4a75bd0a48

작성자 SHA1 메시지 날짜
  Дмитрий Олегович Саенко 4a75bd0a48 Feature: add setting certs for subscription while generating for panel (#3578) 1 개월 전
  Rashid Yusubov b0c223c631 fix: improve russian localization (#3576) 1 개월 전
  Denis Gorelov 313b51f96f feat: Add random Reality Target/SNI selection from 52 popular services (#3577) 1 개월 전
  OleksandrParshyn 020cd63e22 Fix: Graceful Telegram bot shutdown to prevent 409 Conflict (#3580) 1 개월 전
  BOplaid 6e46e9b16e Improve English README (#3579) 1 개월 전

+ 1 - 1
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.
 

+ 14 - 0
main.go

@@ -321,6 +321,20 @@ func updateCert(publicKey string, privateKey string) {
 		} else {
 			fmt.Println("set certificate private key success")
 		}
+
+		err = settingService.SetSubCertFile(publicKey)
+		if err != nil {
+			fmt.Println("set certificate for subscription public key failed:", err)
+		} else {
+			fmt.Println("set certificate for subscription public key success")
+		}
+
+		err = settingService.SetSubKeyFile(privateKey)
+		if err != nil {
+			fmt.Println("set certificate for subscription private key failed:", err)
+		} else {
+			fmt.Println("set certificate for subscription private key success")
+		}
 	} else {
 		fmt.Println("both public and private key should be entered.")
 	}

+ 10 - 2
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;

+ 86 - 0
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
+    };
+}

+ 18 - 2
web/html/form/reality_settings.html

@@ -12,10 +12,26 @@
             <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
         </a-select>
     </a-form-item>
-    <a-form-item label='Target'>
+    <a-form-item>
+        <template slot="label">
+            <a-tooltip>
+                <template slot="title">
+                    <span>{{ i18n "reset" }}</span>
+                </template> Target <a-icon @click="randomizeRealityTarget()"
+                    type="sync"></a-icon>
+            </a-tooltip>
+        </template>
         <a-input v-model.trim="inbound.stream.reality.target"></a-input>
     </a-form-item>
-    <a-form-item label='SNI'>
+    <a-form-item>
+        <template slot="label">
+            <a-tooltip>
+                <template slot="title">
+                    <span>{{ i18n "reset" }}</span>
+                </template> SNI <a-icon @click="randomizeRealityTarget()"
+                    type="sync"></a-icon>
+            </a-tooltip>
+        </template>
         <a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
     </a-form-item>
     <a-form-item label='Max Time Diff (ms)'>

+ 1 - 0
web/html/inbounds.html

@@ -602,6 +602,7 @@
 {{template "page/body_scripts" .}}
 <script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
+<script src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
 <script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
 {{template "component/aSidebar" .}}

+ 7 - 0
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 });

+ 8 - 0
web/service/setting.go

@@ -479,10 +479,18 @@ func (s *SettingService) GetSubDomain() (string, error) {
 	return s.getString("subDomain")
 }
 
+func (s *SettingService) SetSubCertFile(subCertFile string) error {
+	return s.setString("subCertFile", subCertFile)
+}
+
 func (s *SettingService) GetSubCertFile() (string, error) {
 	return s.getString("subCertFile")
 }
 
+func (s *SettingService) SetSubKeyFile(subKeyFile string) error {
+	return s.setString("subKeyFile", subKeyFile)
+}
+
 func (s *SettingService) GetSubKeyFile() (string, error) {
 	return s.getString("subKeyFile")
 }

+ 208 - 155
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(), &params)
+	// 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, &params)
+	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())
-
-	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
-				}
+			t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
+			return nil
+		}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
 
-				client_Id = strings.TrimSpace(message.Text)
-				if t.isSingleWord(client_Id) {
-					userStates[message.Chat.ID] = "awaiting_id"
+		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
 
-					cancel_btn_markup := tu.InlineKeyboard(
-						tu.InlineKeyboardRow(
-							tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
-						),
-					)
+				delete(userStates, message.Chat.ID)
+				t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
+			}()
+			return nil
+		}, th.AnyCommand())
+
+		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
+					}
 
-					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_Id = strings.TrimSpace(message.Text)
+					if t.isSingleWord(client_Id) {
+						userStates[message.Chat.ID] = "awaiting_id"
 
-				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"),
+							),
+						)
 
-					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
+					}
 
-					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_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"),
+							),
+						)
 
-				client_ShPassword = strings.TrimSpace(message.Text)
-				if t.isSingleWord(client_ShPassword) {
-					userStates[message.Chat.ID] = "awaiting_password_sh"
+						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
+					}
 
-					cancel_btn_markup := tu.InlineKeyboard(
-						tu.InlineKeyboardRow(
-							tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
-						),
-					)
+					client_ShPassword = strings.TrimSpace(message.Text)
+					if t.isSingleWord(client_ShPassword) {
+						userStates[message.Chat.ID] = "awaiting_password_sh"
 
-					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
-				}
+						cancel_btn_markup := tu.InlineKeyboard(
+							tu.InlineKeyboardRow(
+								tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
+							),
+						)
 
-				client_Email = strings.TrimSpace(message.Text)
-				if t.isSingleWord(client_Email) {
-					userStates[message.Chat.ID] = "awaiting_email"
+						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
+					}
 
-					cancel_btn_markup := tu.InlineKeyboard(
-						tu.InlineKeyboardRow(
-							tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
-						),
-					)
+					client_Email = strings.TrimSpace(message.Text)
+					if t.isSingleWord(client_Email) {
+						userStates[message.Chat.ID] = "awaiting_email"
 
-					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())
+						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)
 				}
-			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")
+			} 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())
 						}
-						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.

+ 72 - 72
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" = "Нет добавленных балансировщиков."
@@ -83,15 +83,15 @@
 "individualLinks" = "Индивидуальные ссылки"
 "active" = "Активна"
 "inactive" = "Неактивна"
-"unlimited" = "Безлимит"
-"noExpiry" = "Без срока"
+"unlimited" = "Неограниченно"
+"noExpiry" = "Бессрочно"
 
 [menu]
 "theme" = "Тема"
 "dark" = "Темная"
 "ultraDark" = "Очень темная"
 "dashboard" = "Дашборд"
-"inbounds" = "Инбаунды"
+"inbounds" = "Подключения"
 "settings" = "Настройки"
 "xray" = "Настройки Xray"
 "logout" = "Выход"
@@ -107,7 +107,7 @@
 "emptyUsername" = "Введите имя пользователя"
 "emptyPassword" = "Введите пароль"
 "wrongUsernameOrPassword" = "Неверные данные учетной записи."
-"successLogin" = "Вы успешно вошли в аккаунт"
+"successLogin" = "Вход выполнен успешно"
 
 [pages.index]
 "title" = "Дашборд"
@@ -122,7 +122,7 @@
 "stopXray" = "Остановить"
 "restartXray" = "Перезапустить"
 "xraySwitch" = "Выбор версии"
-"xraySwitchClick" = "Выберите желаемую версию"
+"xraySwitchClick" = "Выберите нужную версию"
 "xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки"
 "xrayStatusUnknown" = "Неизвестно"
 "xrayStatusRunning" = "Запущен"
@@ -134,7 +134,7 @@
 "systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
 "connectionCount" = "Количество соединений"
 "ipAddresses" = "IP-адреса сервера"
-"toggleIpVisibility" = "Переключить видимость IP-адресов сервера"
+"toggleIpVisibility" = "Скрыть или показать IP-адреса сервера"
 "overallSpeed" = "Общая скорость передачи трафика"
 "upload" = "Отправка"
 "download" = "Загрузка"
@@ -168,10 +168,10 @@
 [pages.inbounds]
 "allTimeTraffic" = "Общий трафик"
 "allTimeTrafficUsage" = "Общее использование за все время"
-"title" = "Инбаунды"
-"totalDownUp" = "Объем отправленного/полученного трафика"
+"title" = "Подключения"
+"totalDownUp" = "Отправлено/получено"
 "totalUsage" = "Всего трафика"
-"inboundCount" = "Всего инбаундов"
+"inboundCount" = "Всего подключений"
 "operate" = "Меню"
 "enable" = "Включить"
 "remark" = "Примечание"
@@ -185,13 +185,13 @@
 "createdAt" = "Создано"
 "updatedAt" = "Обновлено"
 "resetTraffic" = "Сброс трафика"
-"addInbound" = "Создать инбаунд"
+"addInbound" = "Создать подключение"
 "generalActions" = "Общие действия"
 "autoRefresh" = "Автообновление"
 "autoRefreshInterval" = "Интервал"
-"modifyInbound" = "Изменить инбаунд"
-"deleteInbound" = "Удалить инбаунд"
-"deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?"
+"modifyInbound" = "Изменить подключение"
+"deleteInbound" = "Удалить подключение"
+"deleteInboundContent" = "Вы уверены, что хотите удалить подключение?"
 "deleteClient" = "Удалить клиента"
 "deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
 "resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
@@ -214,11 +214,11 @@
 "export" = "Экспорт ссылок"
 "clone" = "Клонировать"
 "cloneInbound" = "Клонировать"
-"cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания"
+"cloneInboundContent" = "Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания"
 "cloneInboundOk" = "Клонировано"
-"resetAllTraffic" = "Сброс трафика всех инбаундов"
-"resetAllTrafficTitle" = "Сброс трафика всех инбаундов"
-"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?"
+"resetAllTraffic" = "Сброс трафика всех подключений"
+"resetAllTrafficTitle" = "Сброс трафика всех подключений"
+"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?"
 "resetInboundClientTraffics" = "Сброс трафика клиента"
 "resetInboundClientTrafficTitle" = "Сброс трафика клиентов"
 "resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?"
@@ -231,7 +231,7 @@
 "email" = "Email"
 "emailDesc" = "Пожалуйста, укажите уникальный Email"
 "IPLimit" = "Лимит по количеству IP"
-"IPLimitDesc" = "Ограничение количества одновременных подключений с разных IP(0 – отключить)"
+"IPLimitDesc" = "Ограничение числа одновременных подключений с разных IP (0 – отключить)"
 "IPLimitlog" = "Лог IP-адресов"
 "IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)"
 "IPLimitlogclear" = "Очистить лог"
@@ -240,19 +240,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" = "Метод"
@@ -276,13 +276,13 @@
 "obtain" = "Получить"
 "updateSuccess" = "Обновление прошло успешно"
 "logCleanSuccess" = "Лог был очищен"
-"inboundsUpdateSuccess" = "Инбаунды успешно обновлены"
-"inboundUpdateSuccess" = "Инбаунд успешно обновлено"
-"inboundCreateSuccess" = "Инбаунд успешно создано"
-"inboundDeleteSuccess" = "Инбаунд успешно удалено"
-"inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)"
-"inboundClientDeleteSuccess" = "Клиент инбаунда удалён"
-"inboundClientUpdateSuccess" = "Клиент инбаунда обновлён"
+"inboundsUpdateSuccess" = "Подключения успешно обновлены"
+"inboundUpdateSuccess" = "Подключение успешно обновлено"
+"inboundCreateSuccess" = "Подключение успешно создано"
+"inboundDeleteSuccess" = "Подключение успешно удалено"
+"inboundClientAddSuccess" = "Клиент(ы) подключения добавлен(ы)"
+"inboundClientDeleteSuccess" = "Клиент подключения удалён"
+"inboundClientUpdateSuccess" = "Клиент подключения обновлён"
 "delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены"
 "resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен"
 "resetAllTrafficSuccess" = "Весь трафик сброшен"
@@ -310,7 +310,7 @@
 [pages.settings]
 "title" = "Настройки"
 "save" = "Сохранить"
-"infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу."
+"infoDesc" = "Сохраните изменения и перезапустите панель для их применения."
 "restartPanel" = "Перезапуск панели"
 "restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера"
 "restartPanelSuccess" = "Панель успешно перезапущена"
@@ -318,11 +318,11 @@
 "resetDefaultConfig" = "Восстановить настройки по умолчанию"
 "panelSettings" = "Панель"
 "securitySettings" = "Учетная запись"
-"TGBotSettings" = "Telegram"
+"TGBotSettings" = "Telegram-Бот"
 "panelListeningIP" = "IP-адрес для управления панелью"
 "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
 "panelListeningDomain" = "Домен панели"
-"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы подключаться с любых доменов и IP-адресов"
+"panelListeningDomainDesc" = "Оставьте пустым для подключения с любых доменов и IP."
 "panelPort" = "Порт панели"
 "panelPortDesc" = "Порт, на котором работает панель"
 "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
@@ -332,11 +332,11 @@
 "panelUrlPath" = "Корневой путь URL адреса панели"
 "panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'"
 "pageSize" = "Размер нумерации страниц"
-"pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить"
+"pageSizeDesc" = "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить"
 "remarkModel" = "Модель примечания и символ разделения"
-"datepicker" = "Выбор даты"
+"datepicker" = "Тип календаря"
 "datepickerPlaceholder" = "Выберите дату"
-"datepickerDescription" = "Запланированные задачи будут выполняться в выбранное время"
+"datepickerDescription" = "Запланированные задачи будут выполняться в соответствии с этим календарем."
 "sampleRemark" = "Пример примечания"
 "oldUsername" = "Текущий логин"
 "currentPassword" = "Текущий пароль"
@@ -346,7 +346,7 @@
 "telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота"
 "telegramToken" = "Токен Telegram бота"
 "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
-"telegramProxy" = "Прокси Socks5"
+"telegramProxy" = "Прокси-сервер Socks5"
 "telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству."
 "telegramAPIServer" = "API-сервер Telegram"
 "telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию."
@@ -451,11 +451,11 @@
 "RoutingStrategy" = "Настройка маршрутизации доменов"
 "RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
 "Torrent" = "Заблокировать BitTorrent"
-"Inbounds" = "Инбаунды"
+"Inbounds" = "Входящие подключения"
 "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
-"Outbounds" = "Аутбаунды"
+"Outbounds" = "Исходящие подключения"
 "Balancers" = "Балансировщик"
-"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера"
+"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера"
 "Routings" = "Маршрутизация"
 "RoutingsDesc" = "Важен приоритет каждого правила!"
 "completeTemplate" = "Все"
@@ -486,8 +486,8 @@
 "down" = "Опустить вниз"
 "source" = "Источник"
 "dest" = "Пункт назначения"
-"inbound" = "Инбаунд"
-"outbound" = "Аутбаунд"
+"inbound" = "Входящее подключение"
+"outbound" = "Исходящее подключение"
 "balancer" = "Балансировщик"
 "info" = "Информация"
 "add" = "Создать правило"
@@ -495,9 +495,9 @@
 "useComma" = "Элементы, разделённые запятыми"
 
 [pages.xray.outbound]
-"addOutbound" = "Создать аутбаунд"
+"addOutbound" = "Создать исходящее подключение"
 "addReverse" = "Создать реверс-прокси"
-"editOutbound" = "Изменить аутбаунд"
+"editOutbound" = "Изменить исходящее подключение"
 "editReverse" = "Редактировать реверс-прокси"
 "tag" = "Тег"
 "tagDesc" = "Уникальный тег"
@@ -511,7 +511,7 @@
 "intercon" = "Соединение"
 "settings" = "Настройки"
 "accountInfo" = "Информация об учетной записи"
-"outboundStatus" = "Статус аутбаунда"
+"outboundStatus" = "Статус исходящего подключения"
 "sendThrough" = "Отправить через"
 
 [pages.xray.balancer]
@@ -587,8 +587,8 @@
 "modifyUser" = "Вы успешно изменили учетные данные администратора."
 "originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
 "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
-"getOutboundTrafficError" = "Ошибка получения трафика аутбаунда"
-"resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда"
+"getOutboundTrafficError" = "Ошибка получения трафика исходящего подключения"
+"resetOutboundTrafficError" = "Ошибка сброса трафика исходящего подключения"
 
 [tgbot]
 "keyboardClosed" = "❌ Клавиатура закрыта."
@@ -596,7 +596,7 @@
 "noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду."
 "wentWrong" = "❌ Что-то пошло не так..."
 "noIpRecord" = "❗ Нет записей об IP-адресе."
-"noInbounds" = "❗ У вас не настроено ни одного инбаунда."
+"noInbounds" = "❗ У вас не настроено ни одного входящего подключения."
 "unlimited" = "♾ Безлимит"
 "add" = "Добавить"
 "month" = "Месяц"
@@ -606,7 +606,7 @@
 "hours" = "Часов"
 "minutes" = "Минуты"
 "unknown" = "Неизвестно"
-"inbounds" = "Инбаунды"
+"inbounds" = "Входящие подключения"
 "clients" = "Клиенты"
 "offline" = "🔴 Офлайн"
 "online" = "🟢 Онлайн"
@@ -620,7 +620,7 @@
 "status" = "✅ Бот функционирует нормально."
 "usage" = "❗ Пожалуйста, укажите email для поиска."
 "getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>"
-"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска инбаундов (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
+"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска входящих подключений (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
 "helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
 "restartUsage" = "\r\n\r\n<code>/restart</code>"
 "restartSuccess" = "✅ Ядро Xray успешно перезапущено."
@@ -656,7 +656,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"
@@ -685,12 +685,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 }} ]"
@@ -707,7 +707,7 @@
 "confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?"
 "dbBackup" = "📂 Бэкап БД"
 "serverUsage" = "💻 Состояние сервера"
-"getInbounds" = "🔌 Инбаунды"
+"getInbounds" = "🔌 Входящие подключения"
 "depleteSoon" = "⚠️ Скоро конец"
 "clientUsage" = "Статистика клиента"
 "onlines" = "🟢 Онлайн"
@@ -731,7 +731,7 @@
 "allClients" = "👥 Все клиенты"
 "addClient" = "➕ Новый клиент"
 "submitDisable" = "Добавить отключенным ☑️"
-"submitEnable" = "Добавить включенныи ✅"
+"submitEnable" = "Добавить включенным ✅"
 "use_default" = "🏷️ Использовать по умолчанию"
 "change_id" = "⚙️🔑 ID"
 "change_password" = "⚙️🔑 Пароль"
@@ -743,7 +743,7 @@
 [tgbot.answers]
 "successfulOperation" = "✅ Успешно!"
 "errorOperation" = "❗ Ошибка в операции."
-"getInboundsFailed" = "❌ Не удалось получить инбаунды."
+"getInboundsFailed" = "❌ Не удалось получить входящие подключения."
 "getClientsFailed" = "❌ Не удалось получить клиентов."
 "canceled" = "❌ {{ .Email }}: Операция отменена."
 "clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
@@ -760,5 +760,5 @@
 "enableSuccess" = "✅ {{ .Email }}: Включено успешно."
 "disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
 "askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>"
-"chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}"
-"chooseInbound" = "Выберите инбаунд"
+"chooseClient" = "Выберите клиента для входящего подключения {{ .Inbound }}"
+"chooseInbound" = "Выберите входящее подключение"