1
0

tgbot.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607
  1. package tgbot
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "embed"
  6. "math/big"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "regexp"
  11. "slices"
  12. "strconv"
  13. "strings"
  14. "sync"
  15. "time"
  16. "github.com/mhsanaei/3x-ui/v3/internal/eventbus"
  17. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  18. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  19. "github.com/mhsanaei/3x-ui/v3/internal/web/global"
  20. "github.com/mhsanaei/3x-ui/v3/internal/web/locale"
  21. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  22. "github.com/mymmrac/telego"
  23. th "github.com/mymmrac/telego/telegohandler"
  24. "github.com/valyala/fasthttp"
  25. "github.com/valyala/fasthttp/fasthttpproxy"
  26. )
  27. var (
  28. bot *telego.Bot
  29. // botCancel stores the function to cancel the context, stopping Long Polling gracefully.
  30. botCancel context.CancelFunc
  31. // tgBotMutex protects concurrent access to botCancel variable
  32. tgBotMutex sync.Mutex
  33. // botWG waits for the OnReceive Long Polling goroutine to finish.
  34. botWG sync.WaitGroup
  35. botHandler *th.BotHandler
  36. adminIds []int64
  37. isRunning bool
  38. hostname string
  39. hashStorage *global.HashStorage
  40. // EventBus is set from web layer to publish login/security events.
  41. EventBus *eventbus.Bus
  42. // Performance improvements
  43. messageWorkerPool chan struct{} // Semaphore for limiting concurrent message processing
  44. optimizedHTTPClient *http.Client // HTTP client with connection pooling and timeouts
  45. // Simple cache for frequently accessed data
  46. statusCache struct {
  47. data *service.Status
  48. timestamp time.Time
  49. mutex sync.RWMutex
  50. }
  51. serverStatsCache struct {
  52. data string
  53. timestamp time.Time
  54. mutex sync.RWMutex
  55. }
  56. )
  57. // clientDraft is one chat's add-client wizard state. Per-protocol secrets are
  58. // filled per-inbound on submit, so only the universal fields live here.
  59. type clientDraft struct {
  60. sync.Mutex
  61. receiverInboundID int
  62. receiverInboundIDs []int
  63. email string
  64. limitIP int
  65. totalGB int64
  66. expiryTime int64
  67. enable bool
  68. tgID string
  69. subID string
  70. comment string
  71. reset int
  72. }
  73. // chatUser names the admin a wizard belongs to. A private chat's ids are equal;
  74. // in a group they are not, and each admin at its keyboard fills in their own.
  75. type chatUser struct {
  76. chatID int64
  77. userID int64
  78. }
  79. // messageActor reads the sender off a message. A post without one (a channel)
  80. // keys to user 0, an id no admin can hold.
  81. func messageActor(message telego.Message) chatUser {
  82. if message.From == nil {
  83. return chatUser{chatID: message.Chat.ID}
  84. }
  85. return chatUser{chatID: message.Chat.ID, userID: message.From.ID}
  86. }
  87. // callbackActor reads the admin who tapped the button, not the chat the keyboard
  88. // sits in: every admin in a group sees the same keyboard.
  89. func callbackActor(callbackQuery *telego.CallbackQuery) chatUser {
  90. return chatUser{chatID: callbackQuery.Message.GetChat().ID, userID: callbackQuery.From.ID}
  91. }
  92. // clientDrafts keys a draft by the admin filling it in: the steps arrive on the
  93. // worker pool, so one draft let two admins fill in one client between them.
  94. type clientDrafts struct {
  95. mu sync.Mutex
  96. drafts map[chatUser]*clientDraft
  97. }
  98. var addClientDrafts = &clientDrafts{drafts: make(map[chatUser]*clientDraft)}
  99. func (s *clientDrafts) forActor(actor chatUser) *clientDraft {
  100. s.mu.Lock()
  101. defer s.mu.Unlock()
  102. draft, ok := s.drafts[actor]
  103. if !ok {
  104. draft = &clientDraft{}
  105. s.drafts[actor] = draft
  106. }
  107. return draft
  108. }
  109. func (s *clientDrafts) reset(actor chatUser) {
  110. s.mu.Lock()
  111. defer s.mu.Unlock()
  112. delete(s.drafts, actor)
  113. }
  114. // isAddClientStep reports whether callback data belongs to the add-client
  115. // wizard, the only flow that reads or writes a draft.
  116. func isAddClientStep(data string) bool {
  117. return strings.HasPrefix(data, "add_client")
  118. }
  119. func (s *clientDrafts) resetAll() {
  120. s.mu.Lock()
  121. defer s.mu.Unlock()
  122. s.drafts = make(map[chatUser]*clientDraft)
  123. }
  124. // userStateStore guards the per-admin conversation states. The Telegram command
  125. // and callback handlers run on a worker-pool goroutine while the message handler
  126. // runs on the dispatch goroutine, so a bare map would be a concurrent-map-write
  127. // crash. It also expires abandoned conversations so a user who starts a flow and
  128. // goes silent doesn't leave an entry forever.
  129. type userStateStore struct {
  130. mu sync.Mutex
  131. states map[chatUser]userStateEntry
  132. lastPrune time.Time
  133. }
  134. type userStateEntry struct {
  135. state string
  136. at time.Time
  137. }
  138. var userStateMgr = &userStateStore{states: make(map[chatUser]userStateEntry)}
  139. func (s *userStateStore) set(actor chatUser, state string) {
  140. s.mu.Lock()
  141. s.states[actor] = userStateEntry{state: state, at: time.Now()}
  142. s.mu.Unlock()
  143. }
  144. func (s *userStateStore) get(actor chatUser) (string, bool) {
  145. s.mu.Lock()
  146. defer s.mu.Unlock()
  147. e, ok := s.states[actor]
  148. return e.state, ok
  149. }
  150. func (s *userStateStore) clear(actor chatUser) {
  151. s.mu.Lock()
  152. delete(s.states, actor)
  153. s.mu.Unlock()
  154. }
  155. func (s *userStateStore) reset() {
  156. s.mu.Lock()
  157. s.states = make(map[chatUser]userStateEntry)
  158. s.mu.Unlock()
  159. }
  160. // maybePrune drops conversations older than maxAge, at most once per maxAge so a
  161. // busy bot doesn't sweep the whole map on every message.
  162. func (s *userStateStore) maybePrune(maxAge time.Duration) {
  163. s.mu.Lock()
  164. defer s.mu.Unlock()
  165. now := time.Now()
  166. if now.Sub(s.lastPrune) < maxAge {
  167. return
  168. }
  169. s.lastPrune = now
  170. for id, e := range s.states {
  171. if now.Sub(e.at) > maxAge {
  172. delete(s.states, id)
  173. }
  174. }
  175. }
  176. // LoginStatus represents the result of a login attempt.
  177. type LoginStatus byte
  178. // Login status constants
  179. const (
  180. LoginSuccess LoginStatus = 1 // Login was successful
  181. LoginFail LoginStatus = 0 // Login failed
  182. EmptyTelegramUserID = int64(0) // Default value for empty Telegram user ID
  183. )
  184. // LoginAttempt contains safe metadata for panel login notifications.
  185. // It intentionally does not include attempted passwords.
  186. type LoginAttempt struct {
  187. Username string
  188. IP string
  189. Time string
  190. Status LoginStatus
  191. Reason string
  192. }
  193. // Tgbot provides business logic for Telegram bot integration.
  194. // It handles bot commands, user interactions, and status reporting via Telegram.
  195. type Tgbot struct {
  196. inboundService service.InboundService
  197. clientService service.ClientService
  198. settingService service.SettingService
  199. serverService service.ServerService
  200. xrayService service.XrayService
  201. lastStatus *service.Status
  202. }
  203. // NewTgbot creates a new Tgbot instance.
  204. func (t *Tgbot) NewTgbot() *Tgbot {
  205. return new(Tgbot)
  206. }
  207. // I18nBot retrieves a localized message for the bot interface.
  208. func (t *Tgbot) I18nBot(name string, params ...string) string {
  209. return locale.I18n(locale.Bot, name, params...)
  210. }
  211. // GetHashStorage returns the hash storage instance for callback queries.
  212. func (t *Tgbot) GetHashStorage() *global.HashStorage {
  213. return hashStorage
  214. }
  215. // getCachedStatus returns cached server status if it's fresh enough (less than 5 seconds old)
  216. func (t *Tgbot) getCachedStatus() (*service.Status, bool) {
  217. statusCache.mutex.RLock()
  218. defer statusCache.mutex.RUnlock()
  219. if statusCache.data != nil && time.Since(statusCache.timestamp) < 5*time.Second {
  220. return statusCache.data, true
  221. }
  222. return nil, false
  223. }
  224. // setCachedStatus updates the status cache
  225. func (t *Tgbot) setCachedStatus(status *service.Status) {
  226. statusCache.mutex.Lock()
  227. defer statusCache.mutex.Unlock()
  228. statusCache.data = status
  229. statusCache.timestamp = time.Now()
  230. }
  231. // getCachedServerStats returns cached server stats if it's fresh enough (less than 10 seconds old)
  232. func (t *Tgbot) getCachedServerStats() (string, bool) {
  233. serverStatsCache.mutex.RLock()
  234. defer serverStatsCache.mutex.RUnlock()
  235. if serverStatsCache.data != "" && time.Since(serverStatsCache.timestamp) < 10*time.Second {
  236. return serverStatsCache.data, true
  237. }
  238. return "", false
  239. }
  240. // setCachedServerStats updates the server stats cache
  241. func (t *Tgbot) setCachedServerStats(stats string) {
  242. serverStatsCache.mutex.Lock()
  243. defer serverStatsCache.mutex.Unlock()
  244. serverStatsCache.data = stats
  245. serverStatsCache.timestamp = time.Now()
  246. }
  247. // Start initializes and starts the Telegram bot with the provided translation files.
  248. func (t *Tgbot) Start(i18nFS embed.FS) error {
  249. // Initialize localizer
  250. err := locale.InitLocalizer(i18nFS, &t.settingService)
  251. if err != nil {
  252. return err
  253. }
  254. // If Start is called again (e.g. during reload), ensure any previous long-polling
  255. // loop is stopped before creating a new bot / receiver.
  256. StopBot()
  257. // Initialize hash storage to store callback queries
  258. hashStorage = global.NewHashStorage(20 * time.Minute)
  259. // Initialize worker pool for concurrent message processing (max 10 concurrent handlers)
  260. messageWorkerPool = make(chan struct{}, 10)
  261. // Initialize optimized HTTP client with connection pooling
  262. optimizedHTTPClient = &http.Client{
  263. Timeout: 15 * time.Second,
  264. Transport: &http.Transport{
  265. MaxIdleConns: 100,
  266. MaxIdleConnsPerHost: 10,
  267. IdleConnTimeout: 30 * time.Second,
  268. DisableKeepAlives: false,
  269. },
  270. }
  271. t.SetHostname()
  272. // Get Telegram bot token
  273. tgBotToken, err := t.settingService.GetTgBotToken()
  274. if err != nil || tgBotToken == "" {
  275. logger.Warning("Failed to get Telegram bot token:", err)
  276. return err
  277. }
  278. // Get Telegram bot chat ID(s)
  279. tgBotID, err := t.settingService.GetTgBotChatId()
  280. if err != nil {
  281. logger.Warning("Failed to get Telegram bot chat ID:", err)
  282. return err
  283. }
  284. parsedAdminIds := make([]int64, 0)
  285. // Parse admin IDs from comma-separated string
  286. if tgBotID != "" {
  287. for adminID := range strings.SplitSeq(tgBotID, ",") {
  288. id, err := strconv.ParseInt(adminID, 10, 64)
  289. if err != nil {
  290. logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
  291. return err
  292. }
  293. parsedAdminIds = append(parsedAdminIds, id)
  294. }
  295. }
  296. tgBotMutex.Lock()
  297. adminIds = parsedAdminIds
  298. tgBotMutex.Unlock()
  299. // Get Telegram bot proxy URL
  300. tgBotProxy, err := t.settingService.GetTgBotProxy()
  301. if err != nil {
  302. logger.Warning("Failed to get Telegram bot proxy URL:", err)
  303. }
  304. // Fall back to the panel-wide egress bridge when no dedicated bot proxy is
  305. // set. Resolved once at bot start: if Xray comes up later, the bot keeps
  306. // its direct connection until it is restarted.
  307. if tgBotProxy == "" {
  308. if egress := t.settingService.PanelEgressProxyURL(); egress != "" && isSupportedBotProxyScheme(egress) {
  309. tgBotProxy = egress
  310. }
  311. }
  312. // Get Telegram bot API server URL
  313. tgBotAPIServer, err := t.settingService.GetTgBotAPIServer()
  314. if err != nil {
  315. logger.Warning("Failed to get Telegram bot API server URL:", err)
  316. }
  317. // Create new Telegram bot instance
  318. bot, err = t.NewBot(tgBotToken, tgBotProxy, tgBotAPIServer)
  319. if err != nil {
  320. logger.Error("Failed to initialize Telegram bot API:", err)
  321. return err
  322. }
  323. t.trySetBotCommands(bot)
  324. // Start receiving Telegram bot messages
  325. tgBotMutex.Lock()
  326. alreadyRunning := isRunning || botCancel != nil
  327. tgBotMutex.Unlock()
  328. if !alreadyRunning {
  329. logger.Info("Telegram bot receiver started")
  330. go t.OnReceive()
  331. }
  332. return nil
  333. }
  334. func (t *Tgbot) trySetBotCommands(bot *telego.Bot) {
  335. defer func() {
  336. if r := recover(); r != nil {
  337. logger.Warning("Failed to register bot commands (Telegram may be rate-limiting); bot will continue without them:", r)
  338. }
  339. }()
  340. err := bot.SetMyCommands(context.Background(), &telego.SetMyCommandsParams{
  341. Commands: []telego.BotCommand{
  342. {Command: "start", Description: t.I18nBot("tgbot.commands.startDesc")},
  343. {Command: "help", Description: t.I18nBot("tgbot.commands.helpDesc")},
  344. {Command: "status", Description: t.I18nBot("tgbot.commands.statusDesc")},
  345. {Command: "id", Description: t.I18nBot("tgbot.commands.idDesc")},
  346. {Command: "usage", Description: t.I18nBot("tgbot.commands.usageDesc")},
  347. {Command: "inbound", Description: t.I18nBot("tgbot.commands.inboundDesc")},
  348. {Command: "restart", Description: t.I18nBot("tgbot.commands.restartDesc")},
  349. {Command: "clearall", Description: t.I18nBot("tgbot.commands.clearallDesc")},
  350. {Command: "broadcast", Description: t.I18nBot("tgbot.commands.broadcastDesc")},
  351. },
  352. })
  353. if err != nil {
  354. logger.Warning("Failed to set bot commands:", err)
  355. }
  356. }
  357. func isSupportedBotProxyScheme(proxyUrl string) bool {
  358. return strings.HasPrefix(proxyUrl, "socks5://") ||
  359. strings.HasPrefix(proxyUrl, "http://") ||
  360. strings.HasPrefix(proxyUrl, "https://")
  361. }
  362. // createRobustFastHTTPClient creates a fasthttp.Client with proper connection handling
  363. func (t *Tgbot) createRobustFastHTTPClient(proxyUrl string) *fasthttp.Client {
  364. client := &fasthttp.Client{
  365. // Connection timeouts
  366. ReadTimeout: 30 * time.Second,
  367. WriteTimeout: 30 * time.Second,
  368. MaxIdleConnDuration: 60 * time.Second,
  369. MaxConnDuration: 0, // unlimited, but controlled by MaxIdleConnDuration
  370. MaxIdemponentCallAttempts: 3,
  371. ReadBufferSize: 4096,
  372. WriteBufferSize: 4096,
  373. MaxConnsPerHost: 100,
  374. MaxConnWaitTimeout: 10 * time.Second,
  375. DisableHeaderNamesNormalizing: false,
  376. DisablePathNormalizing: false,
  377. // resetTimeout stays false to keep the pre-RetryIfErr retry timing.
  378. RetryIfErr: func(request *fasthttp.Request, _ int, _ error) (bool, bool) {
  379. method := string(request.Header.Method())
  380. return false, method == "GET" || method == "POST"
  381. },
  382. }
  383. if proxyUrl != "" {
  384. if strings.HasPrefix(proxyUrl, "socks5://") {
  385. client.Dial = fasthttpproxy.FasthttpSocksDialer(proxyUrl)
  386. } else {
  387. client.Dial = fasthttpproxy.FasthttpHTTPDialer(proxyUrl)
  388. }
  389. }
  390. return client
  391. }
  392. // NewBot creates a new Telegram bot instance with optional proxy and API server settings.
  393. func (t *Tgbot) NewBot(token string, proxyUrl string, apiServerUrl string) (*telego.Bot, error) {
  394. // Validate proxy URL if provided
  395. if proxyUrl != "" {
  396. if !isSupportedBotProxyScheme(proxyUrl) {
  397. logger.Warning("Unsupported proxy scheme (want socks5:// or http(s)://), ignoring proxy")
  398. proxyUrl = "" // Clear invalid proxy
  399. } else if _, err := url.Parse(proxyUrl); err != nil {
  400. logger.Warningf("Can't parse proxy URL, ignoring proxy: %v", err)
  401. proxyUrl = ""
  402. }
  403. }
  404. // Validate API server URL if provided
  405. if apiServerUrl != "" {
  406. safeURL, err := service.SanitizePublicHTTPURL(apiServerUrl, false)
  407. if err != nil {
  408. logger.Warningf("Invalid or blocked API server URL, using default: %v", err)
  409. apiServerUrl = ""
  410. } else {
  411. apiServerUrl = safeURL
  412. }
  413. }
  414. // Create robust fasthttp client
  415. client := t.createRobustFastHTTPClient(proxyUrl)
  416. // Build bot options
  417. var options []telego.BotOption
  418. options = append(options, telego.WithFastHTTPClient(client))
  419. if apiServerUrl != "" {
  420. options = append(options, telego.WithAPIServer(apiServerUrl))
  421. }
  422. return telego.NewBot(token, options...)
  423. }
  424. // IsRunning checks if the Telegram bot is currently running.
  425. func (t *Tgbot) IsRunning() bool {
  426. tgBotMutex.Lock()
  427. defer tgBotMutex.Unlock()
  428. return isRunning
  429. }
  430. // adminSnapshot returns the admin chat list under the mutex Start and Stop
  431. // replace it under: a torn slice header is not a harmless race.
  432. func adminSnapshot() []int64 {
  433. tgBotMutex.Lock()
  434. defer tgBotMutex.Unlock()
  435. return slices.Clone(adminIds)
  436. }
  437. // SetHostname sets the hostname for the bot.
  438. func (t *Tgbot) SetHostname() {
  439. host, err := os.Hostname()
  440. if err != nil {
  441. logger.Error("get hostname error:", err)
  442. hostname = ""
  443. return
  444. }
  445. hostname = host
  446. }
  447. // Stop safely stops the Telegram bot's Long Polling operation.
  448. // This method now calls the global StopBot function and cleans up other resources.
  449. func (t *Tgbot) Stop() {
  450. StopBot()
  451. logger.Info("Stop Telegram receiver ...")
  452. tgBotMutex.Lock()
  453. adminIds = nil
  454. tgBotMutex.Unlock()
  455. }
  456. // StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
  457. // This is the global function called from main.go's signal handler and t.Stop().
  458. func StopBot() {
  459. // Don't hold the mutex while cancelling/waiting.
  460. tgBotMutex.Lock()
  461. cancel := botCancel
  462. botCancel = nil
  463. handler := botHandler
  464. botHandler = nil
  465. isRunning = false
  466. tgBotMutex.Unlock()
  467. userStateMgr.reset()
  468. addClientDrafts.resetAll()
  469. broadcastResetAll()
  470. if handler != nil {
  471. _ = handler.Stop()
  472. }
  473. if cancel != nil {
  474. logger.Info("Sending cancellation signal to Telegram bot...")
  475. // Cancels the context passed to UpdatesViaLongPolling; this closes updates channel
  476. // and lets botHandler.Start() exit cleanly.
  477. cancel()
  478. botWG.Wait()
  479. logger.Info("Telegram bot successfully stopped.")
  480. }
  481. }
  482. // encodeQuery encodes the query string if it's longer than 64 characters.
  483. func (t *Tgbot) encodeQuery(query string) string {
  484. // NOTE: we only need to hash for more than 64 chars
  485. if len(query) <= 64 {
  486. return query
  487. }
  488. return hashStorage.SaveHash(query)
  489. }
  490. // decodeQuery decodes a hashed query string back to its original form.
  491. func (t *Tgbot) decodeQuery(query string) (string, error) {
  492. if !hashStorage.IsMD5(query) {
  493. return query, nil
  494. }
  495. decoded, exists := hashStorage.GetValue(query)
  496. if !exists {
  497. return "", common.NewError("hash not found in storage!")
  498. }
  499. return decoded, nil
  500. }
  501. // randomLowerAndNum generates a random string of lowercase letters and numbers.
  502. func (t *Tgbot) randomLowerAndNum(length int) string {
  503. charset := "abcdefghijklmnopqrstuvwxyz0123456789"
  504. bytes := make([]byte, length)
  505. for i := range bytes {
  506. randomIndex, _ := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
  507. bytes[i] = charset[randomIndex.Int64()]
  508. }
  509. return string(bytes)
  510. }
  511. // int64Contains checks if an int64 slice contains a specific item.
  512. func int64Contains(slice []int64, item int64) bool {
  513. return slices.Contains(slice, item)
  514. }
  515. // isSingleWord checks if the text contains only a single word.
  516. func (t *Tgbot) isSingleWord(text string) bool {
  517. text = strings.TrimSpace(text)
  518. re := regexp.MustCompile(`\s+`)
  519. return re.MatchString(text)
  520. }