Compare commits

..

1 commit

Author SHA1 Message Date
javadtgh
8945d13e87
Merge 3b262cf180 into 713a7328f6 2025-10-21 14:04:49 +03:00
10 changed files with 238 additions and 431 deletions

View file

@ -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. **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] > [!IMPORTANT]
> 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. > This project is only for personal using, please do not use it for illegal purposes, 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. As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features.

14
main.go
View file

@ -331,20 +331,6 @@ func updateCert(publicKey string, privateKey string) {
} else { } else {
fmt.Println("set certificate private key success") 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 { } else {
fmt.Println("both public and private key should be entered.") fmt.Println("both public and private key should be entered.")
} }

View file

@ -729,8 +729,8 @@ class RealityStreamSettings extends XrayCommonClass {
constructor( constructor(
show = false, show = false,
xver = 0, xver = 0,
target = '', target = 'google.com:443',
serverNames = '', serverNames = 'google.com,www.google.com',
privateKey = '', privateKey = '',
minClientVer = '', minClientVer = '',
maxClientVer = '', maxClientVer = '',
@ -740,14 +740,6 @@ class RealityStreamSettings extends XrayCommonClass {
settings = new RealityStreamSettings.Settings() settings = new RealityStreamSettings.Settings()
) { ) {
super(); 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.show = show;
this.xver = xver; this.xver = xver;
this.target = target; this.target = target;

View file

@ -1,86 +0,0 @@
// 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
};
}

View file

@ -12,26 +12,10 @@
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item label='Target'>
<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-input v-model.trim="inbound.stream.reality.target"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item label='SNI'>
<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-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='Max Time Diff (ms)'> <a-form-item label='Max Time Diff (ms)'>

View file

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

View file

@ -158,13 +158,6 @@
this.inbound.stream.reality.mldsa65Seed = ''; this.inbound.stream.reality.mldsa65Seed = '';
this.inbound.stream.reality.settings.mldsa65Verify = ''; 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() { async getNewEchCert() {
inModal.loading(true); inModal.loading(true);
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni }); const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });

View file

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

View file

@ -38,15 +38,7 @@ import (
) )
var ( 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 botHandler *th.BotHandler
adminIds []int64 adminIds []int64
isRunning bool isRunning bool
@ -314,13 +306,8 @@ func (t *Tgbot) SetHostname() {
hostname = host hostname = host
} }
// Stop safely stops the Telegram bot's Long Polling operation. // Stop stops the Telegram bot and cleans up resources.
// This method now calls the global StopBot function and cleans up other resources.
func (t *Tgbot) Stop() { 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 { if botHandler != nil {
botHandler.Stop() botHandler.Stop()
} }
@ -329,27 +316,6 @@ func (t *Tgbot) Stop() {
adminIds = nil 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. // encodeQuery encodes the query string if it's longer than 64 characters.
func (t *Tgbot) encodeQuery(query string) string { func (t *Tgbot) encodeQuery(query string) string {
// NOTE: we only need to hash for more than 64 chars // NOTE: we only need to hash for more than 64 chars
@ -379,207 +345,188 @@ func (t *Tgbot) OnReceive() {
params := telego.GetUpdatesParams{ params := telego.GetUpdatesParams{
Timeout: 30, // Increased timeout to reduce API calls Timeout: 30, // Increased timeout to reduce API calls
} }
// --- GRACEFUL SHUTDOWN FIX: Context creation ---
tgBotMutex.Lock()
// Create a context with cancellation and store the cancel function. updates, _ := bot.UpdatesViaLongPolling(context.Background(), &params)
var ctx context.Context
// Check if botCancel is already set (to prevent race condition overwrite and goroutine leak) botHandler, _ = th.NewBotHandler(bot, updates)
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() // <<< ИЗМЕНЕНИЕ
}
tgBotMutex.Unlock() 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")))
// Get updates channel using the context. botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
updates, _ := bot.UpdatesViaLongPolling(ctx, &params) // Use goroutine with worker pool for concurrent command processing
botWG.Go(func() { go func() {
messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker
botHandler, _ = th.NewBotHandler(bot, updates)
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
delete(userStates, message.Chat.ID) delete(userStates, message.Chat.ID)
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove()) t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
return nil }()
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard"))) return nil
}, th.AnyCommand())
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
// Use goroutine with worker pool for concurrent command processing // Use goroutine with worker pool for concurrent callback processing
go func() { go func() {
messageWorkerPool <- struct{}{} // Acquire worker messageWorkerPool <- struct{}{} // Acquire worker
defer func() { <-messageWorkerPool }() // Release worker defer func() { <-messageWorkerPool }() // Release worker
delete(userStates, message.Chat.ID) delete(userStates, query.Message.GetChat().ID)
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID)) t.answerCallback(&query, checkAdmin(query.From.ID))
}() }()
return nil return nil
}, th.AnyCommand()) }, th.AnyCallbackQueryWithMessage())
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error { botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
// Use goroutine with worker pool for concurrent callback processing if userState, exists := userStates[message.Chat.ID]; exists {
go func() { switch userState {
messageWorkerPool <- struct{}{} // Acquire worker case "awaiting_id":
defer func() { <-messageWorkerPool }() // Release worker 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
}
delete(userStates, query.Message.GetChat().ID) client_Id = strings.TrimSpace(message.Text)
t.answerCallback(&query, checkAdmin(query.From.ID)) if t.isSingleWord(client_Id) {
}() userStates[message.Chat.ID] = "awaiting_id"
return nil
}, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error { cancel_btn_markup := tu.InlineKeyboard(
if userState, exists := userStates[message.Chat.ID]; exists { tu.InlineKeyboardRow(
switch userState { tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
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) t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
if t.isSingleWord(client_Id) { } else {
userStates[message.Chat.ID] = "awaiting_id" t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 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_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) delete(userStates, message.Chat.ID)
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID) inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol) message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
t.addClient(message.Chat.ID, message_text) 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
}
} else { client_TrPassword = strings.TrimSpace(message.Text)
if message.UsersShared != nil { if t.isSingleWord(client_TrPassword) {
if checkAdmin(message.From.ID) { userStates[message.Chat.ID] = "awaiting_password_tr"
for _, sharedUser := range message.UsersShared.Users {
userID := sharedUser.UserID cancel_btn_markup := tu.InlineKeyboard(
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID) tu.InlineKeyboardRow(
if needRestart { tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
t.xrayService.SetToNeedRestart() ),
} )
output := ""
if err != nil { t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
output += t.I18nBot("tgbot.messages.selectUserFailed") } else {
} else { t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
output += t.I18nBot("tgbot.messages.userSaved") delete(userStates, message.Chat.ID)
} inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove()) 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()
} }
} else { output := ""
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove()) 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())
} }
} }
return nil }
}, th.AnyMessage()) return nil
}, th.AnyMessage())
botHandler.Start() botHandler.Start()
})
} }
// answerCommand processes incoming command messages from Telegram users. // answerCommand processes incoming command messages from Telegram users.

View file

@ -32,7 +32,7 @@
"copySuccess" = "Скопировано" "copySuccess" = "Скопировано"
"sure" = "Да" "sure" = "Да"
"encryption" = "Шифрование" "encryption" = "Шифрование"
"useIPv4ForHost" = "Использовать IPv4 для подключения к хосту" "useIPv4ForHost" = "Использовать IPv4 для хоста"
"transmission" = "Транспорт" "transmission" = "Транспорт"
"host" = "Хост" "host" = "Хост"
"path" = "Путь" "path" = "Путь"
@ -46,8 +46,8 @@
"online" = "Онлайн" "online" = "Онлайн"
"domainName" = "Домен" "domainName" = "Домен"
"monitor" = "Мониторинг IP" "monitor" = "Мониторинг IP"
"certificate" = "SSL-сертификат" "certificate" = "SSL сертификат"
"fail" = "Сбой" "fail" = "Ошибка"
"comment" = "Комментарий" "comment" = "Комментарий"
"success" = "Успешно" "success" = "Успешно"
"lastOnline" = "Был(а) в сети" "lastOnline" = "Был(а) в сети"
@ -55,17 +55,17 @@
"install" = "Установка" "install" = "Установка"
"clients" = "Клиенты" "clients" = "Клиенты"
"usage" = "Использование" "usage" = "Использование"
"twoFactorCode" = "Код 2FA" "twoFactorCode" = "Код"
"remained" = "Остаток" "remained" = "Остаток"
"security" = "Безопасность" "security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности" "secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Соединение не защищено. Не вводите конфиденциальные данные до установки SSL-сертификата." "secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения"
"secAlertConf" = "Некоторые настройки уязвимы. Рекомендуется усилить защиту для предотвращения атак." "secAlertConf" = "Некоторые настройки уязвимы для атак. Чтобы в будущем не было проблем, нужно усилить защиту."
"secAlertSSL" = "Подключение к панели не защищено. Установите SSL-сертификат для защиты данных." "secAlertSSL" = "Ваше подключение к панели не защищено. Установите SSL сертификат для защиты данных."
"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите нестандартный или случайный порт." "secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите случайный или просто другой порт."
"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Настройте уникальный и сложный URI." "secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Сделайте адрес сложным."
"secAlertSubURI" = "URI подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес." "secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес."
"secAlertSubJsonURI" = "URI JSON-подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес." "secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес."
"emptyDnsDesc" = "Нет добавленных DNS-серверов." "emptyDnsDesc" = "Нет добавленных DNS-серверов."
"emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов." "emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов."
"emptyBalancersDesc" = "Нет добавленных балансировщиков." "emptyBalancersDesc" = "Нет добавленных балансировщиков."
@ -83,15 +83,15 @@
"individualLinks" = "Индивидуальные ссылки" "individualLinks" = "Индивидуальные ссылки"
"active" = "Активна" "active" = "Активна"
"inactive" = "Неактивна" "inactive" = "Неактивна"
"unlimited" = "Неограниченно" "unlimited" = "Безлимит"
"noExpiry" = "Бессрочно" "noExpiry" = "Без срока"
[menu] [menu]
"theme" = "Тема" "theme" = "Тема"
"dark" = "Темная" "dark" = "Темная"
"ultraDark" = "Очень темная" "ultraDark" = "Очень темная"
"dashboard" = "Дашборд" "dashboard" = "Дашборд"
"inbounds" = "Подключения" "inbounds" = "Инбаунды"
"settings" = "Настройки" "settings" = "Настройки"
"xray" = "Настройки Xray" "xray" = "Настройки Xray"
"logout" = "Выход" "logout" = "Выход"
@ -107,7 +107,7 @@
"emptyUsername" = "Введите имя пользователя" "emptyUsername" = "Введите имя пользователя"
"emptyPassword" = "Введите пароль" "emptyPassword" = "Введите пароль"
"wrongUsernameOrPassword" = "Неверные данные учетной записи." "wrongUsernameOrPassword" = "Неверные данные учетной записи."
"successLogin" = "Вход выполнен успешно" "successLogin" = "Вы успешно вошли в аккаунт"
[pages.index] [pages.index]
"title" = "Дашборд" "title" = "Дашборд"
@ -122,7 +122,7 @@
"stopXray" = "Остановить" "stopXray" = "Остановить"
"restartXray" = "Перезапустить" "restartXray" = "Перезапустить"
"xraySwitch" = "Выбор версии" "xraySwitch" = "Выбор версии"
"xraySwitchClick" = "Выберите нужную версию" "xraySwitchClick" = "Выберите желаемую версию"
"xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки" "xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки"
"xrayStatusUnknown" = "Неизвестно" "xrayStatusUnknown" = "Неизвестно"
"xrayStatusRunning" = "Запущен" "xrayStatusRunning" = "Запущен"
@ -134,7 +134,7 @@
"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут" "systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
"connectionCount" = "Количество соединений" "connectionCount" = "Количество соединений"
"ipAddresses" = "IP-адреса сервера" "ipAddresses" = "IP-адреса сервера"
"toggleIpVisibility" = "Скрыть или показать IP-адреса сервера" "toggleIpVisibility" = "Переключить видимость IP-адресов сервера"
"overallSpeed" = "Общая скорость передачи трафика" "overallSpeed" = "Общая скорость передачи трафика"
"upload" = "Отправка" "upload" = "Отправка"
"download" = "Загрузка" "download" = "Загрузка"
@ -168,10 +168,10 @@
[pages.inbounds] [pages.inbounds]
"allTimeTraffic" = "Общий трафик" "allTimeTraffic" = "Общий трафик"
"allTimeTrafficUsage" = "Общее использование за все время" "allTimeTrafficUsage" = "Общее использование за все время"
"title" = "Подключения" "title" = "Инбаунды"
"totalDownUp" = "Отправлено/получено" "totalDownUp" = "Объем отправленного/полученного трафика"
"totalUsage" = "Всего трафика" "totalUsage" = "Всего трафика"
"inboundCount" = "Всего подключений" "inboundCount" = "Всего инбаундов"
"operate" = "Меню" "operate" = "Меню"
"enable" = "Включить" "enable" = "Включить"
"remark" = "Примечание" "remark" = "Примечание"
@ -185,13 +185,13 @@
"createdAt" = "Создано" "createdAt" = "Создано"
"updatedAt" = "Обновлено" "updatedAt" = "Обновлено"
"resetTraffic" = "Сброс трафика" "resetTraffic" = "Сброс трафика"
"addInbound" = "Создать подключение" "addInbound" = "Создать инбаунд"
"generalActions" = "Общие действия" "generalActions" = "Общие действия"
"autoRefresh" = "Автообновление" "autoRefresh" = "Автообновление"
"autoRefreshInterval" = "Интервал" "autoRefreshInterval" = "Интервал"
"modifyInbound" = "Изменить подключение" "modifyInbound" = "Изменить инбаунд"
"deleteInbound" = "Удалить подключение" "deleteInbound" = "Удалить инбаунд"
"deleteInboundContent" = "Вы уверены, что хотите удалить подключение?" "deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?"
"deleteClient" = "Удалить клиента" "deleteClient" = "Удалить клиента"
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?" "deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?" "resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
@ -214,11 +214,11 @@
"export" = "Экспорт ссылок" "export" = "Экспорт ссылок"
"clone" = "Клонировать" "clone" = "Клонировать"
"cloneInbound" = "Клонировать" "cloneInbound" = "Клонировать"
"cloneInboundContent" = "Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания" "cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания"
"cloneInboundOk" = "Клонировано" "cloneInboundOk" = "Клонировано"
"resetAllTraffic" = "Сброс трафика всех подключений" "resetAllTraffic" = "Сброс трафика всех инбаундов"
"resetAllTrafficTitle" = "Сброс трафика всех подключений" "resetAllTrafficTitle" = "Сброс трафика всех инбаундов"
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?" "resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?"
"resetInboundClientTraffics" = "Сброс трафика клиента" "resetInboundClientTraffics" = "Сброс трафика клиента"
"resetInboundClientTrafficTitle" = "Сброс трафика клиентов" "resetInboundClientTrafficTitle" = "Сброс трафика клиентов"
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?" "resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?"
@ -231,7 +231,7 @@
"email" = "Email" "email" = "Email"
"emailDesc" = "Пожалуйста, укажите уникальный Email" "emailDesc" = "Пожалуйста, укажите уникальный Email"
"IPLimit" = "Лимит по количеству IP" "IPLimit" = "Лимит по количеству IP"
"IPLimitDesc" = "Ограничение числа одновременных подключений с разных IP (0 отключить)" "IPLimitDesc" = "Ограничение количества одновременных подключений с разных IP(0 отключить)"
"IPLimitlog" = "Лог IP-адресов" "IPLimitlog" = "Лог IP-адресов"
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)" "IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)"
"IPLimitlogclear" = "Очистить лог" "IPLimitlogclear" = "Очистить лог"
@ -240,19 +240,19 @@
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'" "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'"
"info" = "Информация" "info" = "Информация"
"same" = "Тот же" "same" = "Тот же"
"inboundData" = "Данные подключений" "inboundData" = "Данные инбаундов"
"exportInbound" = "Экспорт подключений" "exportInbound" = "Экспорт инбаундов"
"import" = "Импортировать" "import" = "Импортировать"
"importInbound" = "Импорт подключений" "importInbound" = "Импорт инбаундов"
"periodicTrafficResetTitle" = "Сброс трафика" "periodicTrafficResetTitle" = "Сброс трафика"
"periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы" "periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы"
"lastReset" = "Последний сброс" "lastReset" = "Последний сброс"
[pages.client] [pages.client]
"add" = "Добавить клиента" "add" = "Создать клиента"
"edit" = "Редактировать клиента" "edit" = "Редактировать клиента"
"submitAdd" = "Добавить" "submitAdd" = "Добавить"
"submitEdit" = "Сохранить изменения" "submitEdit" = "Сохранить"
"clientCount" = "Количество клиентов" "clientCount" = "Количество клиентов"
"bulk" = "Добавить несколько" "bulk" = "Добавить несколько"
"method" = "Метод" "method" = "Метод"
@ -276,13 +276,13 @@
"obtain" = "Получить" "obtain" = "Получить"
"updateSuccess" = "Обновление прошло успешно" "updateSuccess" = "Обновление прошло успешно"
"logCleanSuccess" = "Лог был очищен" "logCleanSuccess" = "Лог был очищен"
"inboundsUpdateSuccess" = "Подключения успешно обновлены" "inboundsUpdateSuccess" = "Инбаунды успешно обновлены"
"inboundUpdateSuccess" = "Подключение успешно обновлено" "inboundUpdateSuccess" = "Инбаунд успешно обновлено"
"inboundCreateSuccess" = "Подключение успешно создано" "inboundCreateSuccess" = "Инбаунд успешно создано"
"inboundDeleteSuccess" = "Подключение успешно удалено" "inboundDeleteSuccess" = "Инбаунд успешно удалено"
"inboundClientAddSuccess" = "Клиент(ы) подключения добавлен(ы)" "inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)"
"inboundClientDeleteSuccess" = "Клиент подключения удалён" "inboundClientDeleteSuccess" = "Клиент инбаунда удалён"
"inboundClientUpdateSuccess" = "Клиент подключения обновлён" "inboundClientUpdateSuccess" = "Клиент инбаунда обновлён"
"delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены" "delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены"
"resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен" "resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен"
"resetAllTrafficSuccess" = "Весь трафик сброшен" "resetAllTrafficSuccess" = "Весь трафик сброшен"
@ -310,7 +310,7 @@
[pages.settings] [pages.settings]
"title" = "Настройки" "title" = "Настройки"
"save" = "Сохранить" "save" = "Сохранить"
"infoDesc" = "Сохраните изменения и перезапустите панель для их применения." "infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу."
"restartPanel" = "Перезапуск панели" "restartPanel" = "Перезапуск панели"
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера" "restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера"
"restartPanelSuccess" = "Панель успешно перезапущена" "restartPanelSuccess" = "Панель успешно перезапущена"
@ -318,11 +318,11 @@
"resetDefaultConfig" = "Восстановить настройки по умолчанию" "resetDefaultConfig" = "Восстановить настройки по умолчанию"
"panelSettings" = "Панель" "panelSettings" = "Панель"
"securitySettings" = "Учетная запись" "securitySettings" = "Учетная запись"
"TGBotSettings" = "Telegram-Бот" "TGBotSettings" = "Telegram"
"panelListeningIP" = "IP-адрес для управления панелью" "panelListeningIP" = "IP-адрес для управления панелью"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен панели" "panelListeningDomain" = "Домен панели"
"panelListeningDomainDesc" = "Оставьте пустым для подключения с любых доменов и IP." "panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы подключаться с любых доменов и IP-адресов"
"panelPort" = "Порт панели" "panelPort" = "Порт панели"
"panelPortDesc" = "Порт, на котором работает панель" "panelPortDesc" = "Порт, на котором работает панель"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
@ -332,11 +332,11 @@
"panelUrlPath" = "Корневой путь URL адреса панели" "panelUrlPath" = "Корневой путь URL адреса панели"
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'" "panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'"
"pageSize" = "Размер нумерации страниц" "pageSize" = "Размер нумерации страниц"
"pageSizeDesc" = "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить" "pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить"
"remarkModel" = "Модель примечания и символ разделения" "remarkModel" = "Модель примечания и символ разделения"
"datepicker" = "Тип календаря" "datepicker" = "Выбор даты"
"datepickerPlaceholder" = "Выберите дату" "datepickerPlaceholder" = "Выберите дату"
"datepickerDescription" = "Запланированные задачи будут выполняться в соответствии с этим календарем." "datepickerDescription" = "Запланированные задачи будут выполняться в выбранное время"
"sampleRemark" = "Пример примечания" "sampleRemark" = "Пример примечания"
"oldUsername" = "Текущий логин" "oldUsername" = "Текущий логин"
"currentPassword" = "Текущий пароль" "currentPassword" = "Текущий пароль"
@ -346,7 +346,7 @@
"telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота" "telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота"
"telegramToken" = "Токен Telegram бота" "telegramToken" = "Токен Telegram бота"
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather" "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
"telegramProxy" = "Прокси-сервер Socks5" "telegramProxy" = "Прокси Socks5"
"telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству." "telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству."
"telegramAPIServer" = "API-сервер Telegram" "telegramAPIServer" = "API-сервер Telegram"
"telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию." "telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию."
@ -451,11 +451,11 @@
"RoutingStrategy" = "Настройка маршрутизации доменов" "RoutingStrategy" = "Настройка маршрутизации доменов"
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS" "RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
"Torrent" = "Заблокировать BitTorrent" "Torrent" = "Заблокировать BitTorrent"
"Inbounds" = "Входящие подключения" "Inbounds" = "Инбаунды"
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов" "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
"Outbounds" = "Исходящие подключения" "Outbounds" = "Аутбаунды"
"Balancers" = "Балансировщик" "Balancers" = "Балансировщик"
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера" "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера"
"Routings" = "Маршрутизация" "Routings" = "Маршрутизация"
"RoutingsDesc" = "Важен приоритет каждого правила!" "RoutingsDesc" = "Важен приоритет каждого правила!"
"completeTemplate" = "Все" "completeTemplate" = "Все"
@ -486,8 +486,8 @@
"down" = "Опустить вниз" "down" = "Опустить вниз"
"source" = "Источник" "source" = "Источник"
"dest" = "Пункт назначения" "dest" = "Пункт назначения"
"inbound" = "Входящее подключение" "inbound" = "Инбаунд"
"outbound" = "Исходящее подключение" "outbound" = "Аутбаунд"
"balancer" = "Балансировщик" "balancer" = "Балансировщик"
"info" = "Информация" "info" = "Информация"
"add" = "Создать правило" "add" = "Создать правило"
@ -495,9 +495,9 @@
"useComma" = "Элементы, разделённые запятыми" "useComma" = "Элементы, разделённые запятыми"
[pages.xray.outbound] [pages.xray.outbound]
"addOutbound" = "Создать исходящее подключение" "addOutbound" = "Создать аутбаунд"
"addReverse" = "Создать реверс-прокси" "addReverse" = "Создать реверс-прокси"
"editOutbound" = "Изменить исходящее подключение" "editOutbound" = "Изменить аутбаунд"
"editReverse" = "Редактировать реверс-прокси" "editReverse" = "Редактировать реверс-прокси"
"tag" = "Тег" "tag" = "Тег"
"tagDesc" = "Уникальный тег" "tagDesc" = "Уникальный тег"
@ -511,7 +511,7 @@
"intercon" = "Соединение" "intercon" = "Соединение"
"settings" = "Настройки" "settings" = "Настройки"
"accountInfo" = "Информация об учетной записи" "accountInfo" = "Информация об учетной записи"
"outboundStatus" = "Статус исходящего подключения" "outboundStatus" = "Статус аутбаунда"
"sendThrough" = "Отправить через" "sendThrough" = "Отправить через"
[pages.xray.balancer] [pages.xray.balancer]
@ -587,8 +587,8 @@
"modifyUser" = "Вы успешно изменили учетные данные администратора." "modifyUser" = "Вы успешно изменили учетные данные администратора."
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль" "originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены" "userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
"getOutboundTrafficError" = "Ошибка получения трафика исходящего подключения" "getOutboundTrafficError" = "Ошибка получения трафика аутбаунда"
"resetOutboundTrafficError" = "Ошибка сброса трафика исходящего подключения" "resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда"
[tgbot] [tgbot]
"keyboardClosed" = "❌ Клавиатура закрыта." "keyboardClosed" = "❌ Клавиатура закрыта."
@ -596,7 +596,7 @@
"noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду." "noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду."
"wentWrong" = "❌ Что-то пошло не так..." "wentWrong" = "❌ Что-то пошло не так..."
"noIpRecord" = "❗ Нет записей об IP-адресе." "noIpRecord" = "❗ Нет записей об IP-адресе."
"noInbounds" = "❗ У вас не настроено ни одного входящего подключения." "noInbounds" = "❗ У вас не настроено ни одного инбаунда."
"unlimited" = "♾ Безлимит" "unlimited" = "♾ Безлимит"
"add" = "Добавить" "add" = "Добавить"
"month" = "Месяц" "month" = "Месяц"
@ -606,7 +606,7 @@
"hours" = "Часов" "hours" = "Часов"
"minutes" = "Минуты" "minutes" = "Минуты"
"unknown" = "Неизвестно" "unknown" = "Неизвестно"
"inbounds" = "Входящие подключения" "inbounds" = "Инбаунды"
"clients" = "Клиенты" "clients" = "Клиенты"
"offline" = "🔴 Офлайн" "offline" = "🔴 Офлайн"
"online" = "🟢 Онлайн" "online" = "🟢 Онлайн"
@ -620,7 +620,7 @@
"status" = "✅ Бот функционирует нормально." "status" = "✅ Бот функционирует нормально."
"usage" = "❗ Пожалуйста, укажите email для поиска." "usage" = "❗ Пожалуйста, укажите email для поиска."
"getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>" "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>" "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>" "restartUsage" = "\r\n\r\n<code>/restart</code>"
"restartSuccess" = "✅ Ядро Xray успешно перезапущено." "restartSuccess" = "✅ Ядро Xray успешно перезапущено."
@ -656,7 +656,7 @@
"username" = "👤 Имя пользователя: {{ .Username }}\r\n" "username" = "👤 Имя пользователя: {{ .Username }}\r\n"
"password" = "👤 Пароль: {{ .Password }}\r\n" "password" = "👤 Пароль: {{ .Password }}\r\n"
"time" = "⏰ Время: {{ .Time }}\r\n" "time" = "⏰ Время: {{ .Time }}\r\n"
"inbound" = "📍 Входящее подключение: {{ .Remark }}\r\n" "inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
"port" = "🔌 Порт: {{ .Port }}\r\n" "port" = "🔌 Порт: {{ .Port }}\r\n"
"expire" = "📅 Дата окончания: {{ .Time }}\r\n" "expire" = "📅 Дата окончания: {{ .Time }}\r\n"
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n" "expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
@ -685,12 +685,12 @@
"pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль." "pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль."
"email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email." "email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email."
"comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий." "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_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_pass" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!"
"cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄" "cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄"
"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}" "error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}"
"using_default_value" = "Используется значение по умолчанию👌" "using_default_value" = "Используется значение по умолчанию👌"
"incorrect_input" = "Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫" "incorrect_input" ="Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫"
"AreYouSure" = "Вы уверены? 🤔" "AreYouSure" = "Вы уверены? 🤔"
"SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно" "SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно"
"FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]" "FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠 Ошибка: [ {{ .ErrorMessage }} ]"
@ -707,7 +707,7 @@
"confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?" "confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?"
"dbBackup" = "📂 Бэкап БД" "dbBackup" = "📂 Бэкап БД"
"serverUsage" = "💻 Состояние сервера" "serverUsage" = "💻 Состояние сервера"
"getInbounds" = "🔌 Входящие подключения" "getInbounds" = "🔌 Инбаунды"
"depleteSoon" = "⚠️ Скоро конец" "depleteSoon" = "⚠️ Скоро конец"
"clientUsage" = "Статистика клиента" "clientUsage" = "Статистика клиента"
"onlines" = "🟢 Онлайн" "onlines" = "🟢 Онлайн"
@ -731,7 +731,7 @@
"allClients" = "👥 Все клиенты" "allClients" = "👥 Все клиенты"
"addClient" = " Новый клиент" "addClient" = " Новый клиент"
"submitDisable" = "Добавить отключенным ☑️" "submitDisable" = "Добавить отключенным ☑️"
"submitEnable" = "Добавить включенным ✅" "submitEnable" = "Добавить включенныи ✅"
"use_default" = "🏷️ Использовать по умолчанию" "use_default" = "🏷️ Использовать по умолчанию"
"change_id" = "⚙️🔑 ID" "change_id" = "⚙️🔑 ID"
"change_password" = "⚙️🔑 Пароль" "change_password" = "⚙️🔑 Пароль"
@ -743,7 +743,7 @@
[tgbot.answers] [tgbot.answers]
"successfulOperation" = "✅ Успешно!" "successfulOperation" = "✅ Успешно!"
"errorOperation" = "❗ Ошибка в операции." "errorOperation" = "❗ Ошибка в операции."
"getInboundsFailed" = "❌ Не удалось получить входящие подключения." "getInboundsFailed" = "❌ Не удалось получить инбаунды."
"getClientsFailed" = "❌ Не удалось получить клиентов." "getClientsFailed" = "❌ Не удалось получить клиентов."
"canceled" = "❌ {{ .Email }}: Операция отменена." "canceled" = "❌ {{ .Email }}: Операция отменена."
"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен." "clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
@ -760,5 +760,5 @@
"enableSuccess" = "✅ {{ .Email }}: Включено успешно." "enableSuccess" = "✅ {{ .Email }}: Включено успешно."
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно." "disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
"askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>" "askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>"
"chooseClient" = "Выберите клиента для входящего подключения {{ .Inbound }}" "chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}"
"chooseInbound" = "Выберите входящее подключение" "chooseInbound" = "Выберите инбаунд"