mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-25 15:25:01 +03:00
15fee2a035
The bot answered every action with a new message, so a chat filled with stale menus whose buttons still acted, and long client or inbound lists were paged through dozens of buttons. Each chat now has one live screen that is edited in place: the menu is grouped into Server, Clients, Traffic and Maintenance, lists open through inline search, the add-client flow starts from a draft card, QR codes arrive as one labelled sheet, and repeated notices collapse into one with a counter. A client's /start and /usage show only that client's own links and usage, groups are refused, and the settings page warns when the bot's inline mode is off. Admin topics, the indexed Telegram lookup and every locale, Slovak included, carry over.
170 lines
5.1 KiB
Go
170 lines
5.1 KiB
Go
package tgbot
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/eventbus"
|
|
)
|
|
|
|
var cachedHostname string
|
|
|
|
func getHostname() string {
|
|
if cachedHostname != "" {
|
|
return cachedHostname
|
|
}
|
|
h, err := os.Hostname()
|
|
if err != nil {
|
|
cachedHostname = "unknown"
|
|
} else {
|
|
cachedHostname = h
|
|
}
|
|
return cachedHostname
|
|
}
|
|
|
|
// HandleEvent is the eventbus subscriber callback: one kind of event keeps ONE
|
|
// live card per admin chat, so a repeat edits it instead of a wall of messages.
|
|
func (t *Tgbot) HandleEvent(e eventbus.Event) {
|
|
if !t.isEventEnabled(e.Type) {
|
|
return
|
|
}
|
|
msg := t.formatEventMessage(e)
|
|
if msg == "" {
|
|
return
|
|
}
|
|
kind := t.eventNoticeKind(e)
|
|
for _, adminID := range adminSnapshot() {
|
|
t.liveNotice(adminID, kind, msg)
|
|
}
|
|
}
|
|
|
|
// eventNoticeKind is the live card a repeat of this event edits. A login success
|
|
// must not fold into the failure card from the same IP: outcome is part of the key.
|
|
func (t *Tgbot) eventNoticeKind(e eventbus.Event) string {
|
|
if e.Type == eventbus.EventLoginAttempt {
|
|
if data, ok := e.Data.(*eventbus.LoginEventData); ok {
|
|
return string(e.Type) + ":" + e.Source + ":" + data.Status
|
|
}
|
|
}
|
|
return string(e.Type) + ":" + e.Source
|
|
}
|
|
|
|
func (t *Tgbot) isEventEnabled(eventType eventbus.EventType) bool {
|
|
events, err := t.settingService.GetTgEnabledEvents()
|
|
if err != nil || events == "" {
|
|
return false
|
|
}
|
|
for e := range strings.SplitSeq(events, ",") {
|
|
if strings.TrimSpace(e) == string(eventType) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (t *Tgbot) formatEventMessage(e eventbus.Event) string {
|
|
host := getHostname()
|
|
header := fmt.Sprintf("<b>📡 %s</b>\n", host)
|
|
|
|
switch e.Type {
|
|
case eventbus.EventOutboundDown:
|
|
msg := header + t.I18nBot("tgbot.messages.eventOutboundDown",
|
|
"Tag=="+e.Source)
|
|
if data, ok := e.Data.(*eventbus.OutboundHealthData); ok {
|
|
if data.Error != "" {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventErrorDetail",
|
|
"Error=="+data.Error)
|
|
}
|
|
if data.Delay > 0 {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventDelayDetail",
|
|
"Delay=="+fmt.Sprintf("%d", data.Delay))
|
|
}
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventOutboundUp:
|
|
msg := header + t.I18nBot("tgbot.messages.eventOutboundUp",
|
|
"Tag=="+e.Source)
|
|
if data, ok := e.Data.(*eventbus.OutboundHealthData); ok && data.Delay > 0 {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventDelayDetail",
|
|
"Delay=="+fmt.Sprintf("%d", data.Delay))
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventXrayCrash:
|
|
errStr := ""
|
|
if e.Data != nil {
|
|
errStr = fmt.Sprint(e.Data)
|
|
}
|
|
msg := header + "🔥 " + t.I18nBot("tgbot.messages.eventXrayCrash")
|
|
if errStr != "" {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventXrayCrashError", "Error=="+errStr)
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventNodeDown:
|
|
msg := header + "🔴 " + t.I18nBot("tgbot.messages.eventNodeDown", "Name=="+e.Source)
|
|
if data, ok := e.Data.(*eventbus.NodeHealthData); ok && data.XrayError != "" {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventErrorDetail", "Error=="+data.XrayError)
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventNodeUp:
|
|
msg := header + "🟢 " + t.I18nBot("tgbot.messages.eventNodeUp", "Name=="+e.Source)
|
|
if data, ok := e.Data.(*eventbus.NodeHealthData); ok && data.LatencyMs > 0 {
|
|
msg += "\n" + t.I18nBot("tgbot.messages.eventDelayDetail", "Delay=="+fmt.Sprintf("%d", data.LatencyMs))
|
|
}
|
|
return msg
|
|
|
|
case eventbus.EventCPUHigh:
|
|
if data, ok := e.Data.(*eventbus.SystemMetricData); ok {
|
|
tgCpu, err := t.settingService.GetTgCpu()
|
|
if err != nil || tgCpu <= 0 || data.Percent <= float64(tgCpu) {
|
|
return ""
|
|
}
|
|
return header + "🔴 " + t.I18nBot("tgbot.messages.cpuThreshold",
|
|
"Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64),
|
|
"Threshold=="+strconv.Itoa(tgCpu))
|
|
}
|
|
return ""
|
|
|
|
case eventbus.EventMemoryHigh:
|
|
if data, ok := e.Data.(*eventbus.SystemMetricData); ok {
|
|
tgMemory, err := t.settingService.GetTgMemory()
|
|
if err != nil || tgMemory <= 0 || data.Percent <= float64(tgMemory) {
|
|
return ""
|
|
}
|
|
return header + "🔴 " + t.I18nBot("tgbot.messages.memoryThreshold",
|
|
"Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64),
|
|
"Threshold=="+strconv.Itoa(tgMemory))
|
|
}
|
|
return ""
|
|
|
|
case eventbus.EventLoginAttempt:
|
|
if data, ok := e.Data.(*eventbus.LoginEventData); ok {
|
|
if data.Status == "success" {
|
|
msg := t.I18nBot("tgbot.messages.loginSuccess")
|
|
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+host)
|
|
msg += t.I18nBot("tgbot.messages.username", "Username=="+data.Username)
|
|
msg += t.I18nBot("tgbot.messages.ip", "IP=="+data.IP)
|
|
msg += t.I18nBot("tgbot.messages.time", "Time=="+data.Time)
|
|
return msg
|
|
}
|
|
msg := t.I18nBot("tgbot.messages.loginFailed")
|
|
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+host)
|
|
if data.Reason != "" {
|
|
msg += t.I18nBot("tgbot.messages.reason", "Reason=="+data.Reason)
|
|
}
|
|
msg += t.I18nBot("tgbot.messages.username", "Username=="+data.Username)
|
|
msg += t.I18nBot("tgbot.messages.ip", "IP=="+data.IP)
|
|
msg += t.I18nBot("tgbot.messages.time", "Time=="+data.Time)
|
|
return msg
|
|
}
|
|
return header + t.I18nBot("tgbot.messages.eventLoginFallback", "Source=="+e.Source)
|
|
}
|
|
|
|
return ""
|
|
}
|