mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-11-02 19:25:05 +03:00
d63583caaf
The Telegram login alert is a list of "label: value" lines, and the username, whatever the visitor typed on the login form, was inserted with its line breaks intact. Text after a break read as the panel's own IP and time lines, letting a failed login forge the alert. Control characters and Unicode line separators in the username become spaces, in the failed and the successful alert alike.
182 lines
5.4 KiB
Go
182 lines
5.4 KiB
Go
package tgbot
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
"unicode"
|
||
|
||
"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
|
||
}
|
||
|
||
// oneLine keeps a value the panel did not write itself on its own line of an
|
||
// alert: control characters and Unicode line separators become spaces.
|
||
func oneLine(s string) string {
|
||
return strings.Map(func(r rune) rune {
|
||
if unicode.IsControl(r) || r == '
' || r == '
' {
|
||
return ' '
|
||
}
|
||
return r
|
||
}, s)
|
||
}
|
||
|
||
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=="+oneLine(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=="+oneLine(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 ""
|
||
}
|