tgbot_client.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. package tgbot
  2. import (
  3. "context"
  4. "encoding/base64"
  5. "encoding/json"
  6. "errors"
  7. "fmt"
  8. "io"
  9. "net/http"
  10. "slices"
  11. "strconv"
  12. "strings"
  13. "time"
  14. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  15. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  16. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  17. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  18. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  19. "github.com/mymmrac/telego"
  20. tu "github.com/mymmrac/telego/telegoutil"
  21. "github.com/skip2/go-qrcode"
  22. )
  23. // BuildClientDraftMessage builds a protocol-neutral summary of the in-progress
  24. // client (email, attached inbounds, traffic limit, expiry, ip limit, comment)
  25. // shown in the multi-inbound add flow. Per-protocol secrets (UUID, password,
  26. // flow, method) are generated by fillProtocolDefaults on submit, so the bot
  27. // never has to track them per inbound itself.
  28. func (t *Tgbot) BuildClientDraftMessage() string {
  29. now := time.Now().UnixMilli()
  30. expiry := ""
  31. switch {
  32. case client_ExpiryTime == 0:
  33. expiry = t.I18nBot("tgbot.unlimited")
  34. case client_ExpiryTime < 0:
  35. expiry = fmt.Sprintf("%d %s", client_ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
  36. default:
  37. diff := client_ExpiryTime - now
  38. if diff > 172800000 {
  39. expiry = time.UnixMilli(client_ExpiryTime).Format("2006-01-02 15:04:05")
  40. } else {
  41. expiry = fmt.Sprintf("%d %s", diff/3600000, t.I18nBot("tgbot.hours"))
  42. }
  43. }
  44. traffic := "♾️ Unlimited(Reset)"
  45. if client_TotalGB > 0 {
  46. traffic = common.FormatTraffic(client_TotalGB)
  47. }
  48. ipLimit := "♾️ Unlimited(Reset)"
  49. if client_LimitIP > 0 {
  50. ipLimit = fmt.Sprint(client_LimitIP)
  51. }
  52. attached := t.describeAttachedInbounds(receiver_inbound_IDs)
  53. if attached == "" {
  54. attached = "—"
  55. }
  56. comment := client_Comment
  57. if comment == "" {
  58. comment = "—"
  59. }
  60. tgID := client_TgID
  61. if tgID == "" {
  62. tgID = "—"
  63. }
  64. var b strings.Builder
  65. b.WriteString("📝 *New client draft*\r\n")
  66. b.WriteString(fmt.Sprintf("📧 Email: `%s`\r\n", client_Email))
  67. b.WriteString(fmt.Sprintf("🔗 Attached: %s\r\n", attached))
  68. b.WriteString(fmt.Sprintf("📊 Traffic: %s\r\n", traffic))
  69. b.WriteString(fmt.Sprintf("📅 Expire: %s\r\n", expiry))
  70. b.WriteString(fmt.Sprintf("🔢 IP limit: %s\r\n", ipLimit))
  71. b.WriteString(fmt.Sprintf("👤 TG user: %s\r\n", tgID))
  72. b.WriteString(fmt.Sprintf("💬 Comment: %s\r\n", comment))
  73. return b.String()
  74. }
  75. // describeAttachedInbounds returns a short "remark1, remark2" list for the given
  76. // inbound ids, falling back to "#id" when an inbound can't be loaded.
  77. func (t *Tgbot) describeAttachedInbounds(ids []int) string {
  78. if len(ids) == 0 {
  79. return ""
  80. }
  81. parts := make([]string, 0, len(ids))
  82. for _, id := range ids {
  83. ib, err := t.inboundService.GetInbound(id)
  84. if err != nil || ib == nil {
  85. parts = append(parts, fmt.Sprintf("#%d", id))
  86. continue
  87. }
  88. label := ib.Remark
  89. if label == "" {
  90. label = fmt.Sprintf("#%d", id)
  91. }
  92. parts = append(parts, label)
  93. }
  94. return strings.Join(parts, ", ")
  95. }
  96. // SubmitAddClient sends the in-progress client to ClientService.Create with
  97. // the full set of attached inbound ids. Per-inbound fillProtocolDefaults on
  98. // the panel generates UUID/password/auth per protocol, so the bot only
  99. // supplies the universal fields it actually collected.
  100. func (t *Tgbot) SubmitAddClient() (bool, error) {
  101. inboundIDs := receiver_inbound_IDs
  102. if len(inboundIDs) == 0 && receiver_inbound_ID > 0 {
  103. inboundIDs = []int{receiver_inbound_ID}
  104. }
  105. if len(inboundIDs) == 0 {
  106. return false, errors.New(t.I18nBot("tgbot.answers.getInboundsFailed"))
  107. }
  108. tgIDInt, _ := strconv.ParseInt(client_TgID, 10, 64)
  109. client := model.Client{
  110. Email: client_Email,
  111. Enable: client_Enable,
  112. LimitIP: client_LimitIP,
  113. TotalGB: client_TotalGB,
  114. ExpiryTime: client_ExpiryTime,
  115. SubID: client_SubID,
  116. Comment: client_Comment,
  117. Reset: client_Reset,
  118. TgID: tgIDInt,
  119. }
  120. return t.clientService.Create(&t.inboundService, &service.ClientCreatePayload{
  121. Client: client,
  122. InboundIds: inboundIDs,
  123. })
  124. }
  125. // buildSubscriptionURLs builds the HTML sub page URL and JSON subscription URL for a client email
  126. func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
  127. // Resolve subId from client email
  128. traffic, client, err := t.inboundService.GetClientByEmail(email)
  129. _ = traffic
  130. if err != nil || client == nil {
  131. return "", "", errors.New("client not found")
  132. }
  133. // Gather settings to construct absolute URLs
  134. subURI, _ := t.settingService.GetSubURI()
  135. subJsonURI, _ := t.settingService.GetSubJsonURI()
  136. subDomain, _ := t.settingService.GetSubDomain()
  137. subPort, _ := t.settingService.GetSubPort()
  138. subPath, _ := t.settingService.GetSubPath()
  139. subJsonPath, _ := t.settingService.GetSubJsonPath()
  140. subJsonEnable, _ := t.settingService.GetSubJsonEnable()
  141. subKeyFile, _ := t.settingService.GetSubKeyFile()
  142. subCertFile, _ := t.settingService.GetSubCertFile()
  143. tls := (subKeyFile != "" && subCertFile != "")
  144. scheme := "http"
  145. if tls {
  146. scheme = "https"
  147. }
  148. // Fallbacks
  149. if subDomain == "" {
  150. // try panel domain, otherwise OS hostname
  151. if d, err := t.settingService.GetWebDomain(); err == nil && d != "" {
  152. subDomain = d
  153. } else if hostname != "" {
  154. subDomain = hostname
  155. } else {
  156. subDomain = "localhost"
  157. }
  158. }
  159. host := subDomain
  160. if (subPort == 443 && tls) || (subPort == 80 && !tls) {
  161. // standard ports: no port in host
  162. } else {
  163. host = fmt.Sprintf("%s:%d", subDomain, subPort)
  164. }
  165. // Ensure paths
  166. if !strings.HasPrefix(subPath, "/") {
  167. subPath = "/" + subPath
  168. }
  169. if !strings.HasSuffix(subPath, "/") {
  170. subPath = subPath + "/"
  171. }
  172. if !strings.HasPrefix(subJsonPath, "/") {
  173. subJsonPath = "/" + subJsonPath
  174. }
  175. if !strings.HasSuffix(subJsonPath, "/") {
  176. subJsonPath = subJsonPath + "/"
  177. }
  178. var subURL string
  179. var subJsonURL string
  180. // If pre-configured URIs are available, use them directly
  181. if subURI != "" {
  182. if !strings.HasSuffix(subURI, "/") {
  183. subURI = subURI + "/"
  184. }
  185. subURL = fmt.Sprintf("%s%s", subURI, client.SubID)
  186. } else {
  187. subURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
  188. }
  189. if subJsonURI != "" {
  190. if !strings.HasSuffix(subJsonURI, "/") {
  191. subJsonURI = subJsonURI + "/"
  192. }
  193. subJsonURL = fmt.Sprintf("%s%s", subJsonURI, client.SubID)
  194. } else {
  195. subJsonURL = fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
  196. }
  197. if !subJsonEnable {
  198. subJsonURL = ""
  199. }
  200. return subURL, subJsonURL, nil
  201. }
  202. // sendClientSubLinks sends the subscription links for the client to the chat.
  203. func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
  204. subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
  205. if err != nil {
  206. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
  207. return
  208. }
  209. msg := "Subscription URL:\r\n<code>" + subURL + "</code>"
  210. if subJsonURL != "" {
  211. msg += "\r\n\r\nJSON URL:\r\n<code>" + subJsonURL + "</code>"
  212. }
  213. inlineKeyboard := tu.InlineKeyboard(
  214. tu.InlineKeyboardRow(
  215. tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links "+email)),
  216. ),
  217. tu.InlineKeyboardRow(
  218. tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("client_qr_links "+email)),
  219. ),
  220. )
  221. t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
  222. }
  223. // sendClientIndividualLinks fetches the subscription content (individual links) and sends it to the user
  224. func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
  225. // Build the HTML sub page URL; we'll call it with header Accept to get raw content
  226. subURL, _, err := t.buildSubscriptionURLs(email)
  227. if err != nil {
  228. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
  229. return
  230. }
  231. // Try to fetch raw subscription links. Prefer plain text response.
  232. req, err := http.NewRequest("GET", subURL, nil)
  233. if err != nil {
  234. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
  235. return
  236. }
  237. // Force plain text to avoid HTML page; controller respects Accept header
  238. req.Header.Set("Accept", "text/plain, */*;q=0.1")
  239. // Use optimized client with connection pooling
  240. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  241. defer cancel()
  242. req = req.WithContext(ctx)
  243. resp, err := optimizedHTTPClient.Do(req)
  244. if err != nil {
  245. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
  246. return
  247. }
  248. defer resp.Body.Close()
  249. bodyBytes, err := io.ReadAll(resp.Body)
  250. if err != nil {
  251. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
  252. return
  253. }
  254. // If service is configured to encode (Base64), decode it
  255. encoded, _ := t.settingService.GetSubEncrypt()
  256. var content string
  257. if encoded {
  258. decoded, err := base64.StdEncoding.DecodeString(string(bodyBytes))
  259. if err != nil {
  260. // fallback to raw text
  261. content = string(bodyBytes)
  262. } else {
  263. content = string(decoded)
  264. }
  265. } else {
  266. content = string(bodyBytes)
  267. }
  268. // Normalize line endings and trim
  269. lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
  270. var cleaned []string
  271. for _, l := range lines {
  272. l = strings.TrimSpace(l)
  273. if l != "" {
  274. cleaned = append(cleaned, l)
  275. }
  276. }
  277. if len(cleaned) == 0 {
  278. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noResult"))
  279. return
  280. }
  281. // Send in chunks to respect message length; use monospace formatting
  282. const maxPerMessage = 50
  283. for i := 0; i < len(cleaned); i += maxPerMessage {
  284. j := min(i+maxPerMessage, len(cleaned))
  285. chunk := cleaned[i:j]
  286. var msg strings.Builder
  287. msg.WriteString(t.I18nBot("subscription.individualLinks"))
  288. msg.WriteString(":\r\n")
  289. for _, link := range chunk {
  290. // wrap each link in <code>
  291. msg.WriteString("<code>")
  292. msg.WriteString(link)
  293. msg.WriteString("</code>\r\n")
  294. }
  295. t.SendMsgToTgbot(chatId, msg.String())
  296. }
  297. }
  298. // sendClientQRLinks generates QR images for subscription URL, JSON URL, and a few individual links, then sends them
  299. func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
  300. subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
  301. if err != nil {
  302. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
  303. return
  304. }
  305. // Helper to create QR PNG bytes from content
  306. createQR := func(content string, size int) ([]byte, error) {
  307. if size <= 0 {
  308. size = 256
  309. }
  310. return qrcode.Encode(content, qrcode.Medium, size)
  311. }
  312. // Inform user
  313. t.SendMsgToTgbot(chatId, "QRCode for client "+email+":")
  314. // Send sub URL QR (filename: sub.png)
  315. if png, err := createQR(subURL, 320); err == nil {
  316. document := tu.Document(
  317. tu.ID(chatId),
  318. tu.FileFromBytes(png, "sub.png"),
  319. )
  320. _, _ = bot.SendDocument(context.Background(), document)
  321. } else {
  322. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
  323. }
  324. // Send JSON URL QR (filename: subjson.png) when available
  325. if subJsonURL != "" {
  326. if png, err := createQR(subJsonURL, 320); err == nil {
  327. document := tu.Document(
  328. tu.ID(chatId),
  329. tu.FileFromBytes(png, "subjson.png"),
  330. )
  331. _, _ = bot.SendDocument(context.Background(), document)
  332. } else {
  333. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
  334. }
  335. }
  336. // Also generate a few individual links' QRs (first up to 5)
  337. subPageURL := subURL
  338. req, err := http.NewRequest("GET", subPageURL, nil)
  339. if err == nil {
  340. req.Header.Set("Accept", "text/plain, */*;q=0.1")
  341. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  342. defer cancel()
  343. req = req.WithContext(ctx)
  344. if resp, err := optimizedHTTPClient.Do(req); err == nil {
  345. body, _ := io.ReadAll(resp.Body)
  346. _ = resp.Body.Close()
  347. encoded, _ := t.settingService.GetSubEncrypt()
  348. var content string
  349. if encoded {
  350. if dec, err := base64.StdEncoding.DecodeString(string(body)); err == nil {
  351. content = string(dec)
  352. } else {
  353. content = string(body)
  354. }
  355. } else {
  356. content = string(body)
  357. }
  358. lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
  359. var cleaned []string
  360. for _, l := range lines {
  361. l = strings.TrimSpace(l)
  362. if l != "" {
  363. cleaned = append(cleaned, l)
  364. }
  365. }
  366. if len(cleaned) > 0 {
  367. max := min(len(cleaned), 5)
  368. for i := range max {
  369. if png, err := createQR(cleaned[i], 320); err == nil {
  370. // Use the email as filename for individual link QR
  371. filename := email + ".png"
  372. document := tu.Document(
  373. tu.ID(chatId),
  374. tu.FileFromBytes(png, filename),
  375. )
  376. _, _ = bot.SendDocument(context.Background(), document)
  377. // Reduced delay for better performance
  378. if i < max-1 { // Only delay between documents, not after the last one
  379. time.Sleep(50 * time.Millisecond)
  380. }
  381. }
  382. }
  383. }
  384. }
  385. }
  386. }
  387. // clientInfoMsg formats client information message based on traffic and flags.
  388. func (t *Tgbot) clientInfoMsg(
  389. traffic *xray.ClientTraffic,
  390. printEnabled bool,
  391. printOnline bool,
  392. printActive bool,
  393. printDate bool,
  394. printTraffic bool,
  395. printRefreshed bool,
  396. ) string {
  397. now := time.Now().Unix()
  398. expiryTime := ""
  399. flag := false
  400. diff := traffic.ExpiryTime/1000 - now
  401. if traffic.ExpiryTime == 0 {
  402. expiryTime = t.I18nBot("tgbot.unlimited")
  403. } else if diff > 172800 || !traffic.Enable {
  404. expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
  405. if diff > 0 {
  406. days := diff / 86400
  407. hours := (diff % 86400) / 3600
  408. minutes := (diff % 3600) / 60
  409. remainingTime := ""
  410. if days > 0 {
  411. remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days"))
  412. }
  413. if hours > 0 {
  414. remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours"))
  415. }
  416. if minutes > 0 {
  417. remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes"))
  418. }
  419. expiryTime += fmt.Sprintf(" (%s)", remainingTime)
  420. }
  421. } else if traffic.ExpiryTime < 0 {
  422. expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
  423. flag = true
  424. } else {
  425. expiryTime = fmt.Sprintf("%d %s", diff/3600, t.I18nBot("tgbot.hours"))
  426. flag = true
  427. }
  428. total := ""
  429. if traffic.Total == 0 {
  430. total = t.I18nBot("tgbot.unlimited")
  431. } else {
  432. total = common.FormatTraffic((traffic.Total))
  433. }
  434. enabled := ""
  435. isEnabled, err := t.clientService.CheckIsEnabledByEmail(&t.inboundService, traffic.Email)
  436. if err != nil {
  437. logger.Warning(err)
  438. enabled = t.I18nBot("tgbot.wentWrong")
  439. } else if isEnabled {
  440. enabled = t.I18nBot("tgbot.messages.yes")
  441. } else {
  442. enabled = t.I18nBot("tgbot.messages.no")
  443. }
  444. active := ""
  445. if traffic.Enable {
  446. active = t.I18nBot("tgbot.messages.yes")
  447. } else {
  448. active = t.I18nBot("tgbot.messages.no")
  449. }
  450. status := t.I18nBot("tgbot.offline")
  451. isOnline := false
  452. if service.XrayProcess().IsRunning() {
  453. if slices.Contains(service.XrayProcess().GetOnlineClients(), traffic.Email) {
  454. status = t.I18nBot("tgbot.online")
  455. isOnline = true
  456. }
  457. }
  458. output := ""
  459. output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
  460. if attachIds, err := t.clientService.GetInboundIdsForEmail(nil, traffic.Email); err == nil && len(attachIds) > 0 {
  461. output += fmt.Sprintf("🔗 Inbounds: %s\r\n", t.describeAttachedInbounds(attachIds))
  462. }
  463. if printEnabled {
  464. output += t.I18nBot("tgbot.messages.enabled", "Enable=="+enabled)
  465. }
  466. if printOnline {
  467. output += t.I18nBot("tgbot.messages.online", "Status=="+status)
  468. if !isOnline && traffic.LastOnline > 0 {
  469. output += t.I18nBot("tgbot.messages.lastOnline", "Time=="+time.UnixMilli(traffic.LastOnline).Format("2006-01-02 15:04:05"))
  470. }
  471. }
  472. if printActive {
  473. output += t.I18nBot("tgbot.messages.active", "Enable=="+active)
  474. }
  475. if printDate {
  476. if flag {
  477. output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
  478. } else {
  479. output += t.I18nBot("tgbot.messages.expire", "Time=="+expiryTime)
  480. }
  481. }
  482. if printTraffic {
  483. output += t.I18nBot("tgbot.messages.upload", "Upload=="+common.FormatTraffic(traffic.Up))
  484. output += t.I18nBot("tgbot.messages.download", "Download=="+common.FormatTraffic(traffic.Down))
  485. output += t.I18nBot("tgbot.messages.total", "UpDown=="+common.FormatTraffic((traffic.Up+traffic.Down)), "Total=="+total)
  486. }
  487. if printRefreshed {
  488. output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
  489. }
  490. return output
  491. }
  492. // getClientUsage retrieves and sends client usage information to the chat.
  493. func (t *Tgbot) getClientUsage(chatId int64, tgUserID int64, email ...string) {
  494. traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
  495. if err != nil {
  496. logger.Warning(err)
  497. msg := t.I18nBot("tgbot.wentWrong")
  498. t.SendMsgToTgbot(chatId, msg)
  499. return
  500. }
  501. if len(traffics) == 0 {
  502. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
  503. return
  504. }
  505. output := ""
  506. if len(traffics) > 0 {
  507. if len(email) > 0 {
  508. for _, traffic := range traffics {
  509. if traffic.Email == email[0] {
  510. output := t.clientInfoMsg(traffic, true, true, true, true, true, true)
  511. t.SendMsgToTgbot(chatId, output)
  512. return
  513. }
  514. }
  515. msg := t.I18nBot("tgbot.noResult")
  516. t.SendMsgToTgbot(chatId, msg)
  517. return
  518. } else {
  519. for _, traffic := range traffics {
  520. output += t.clientInfoMsg(traffic, true, true, true, true, true, false)
  521. output += "\r\n"
  522. }
  523. }
  524. }
  525. output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
  526. t.SendMsgToTgbot(chatId, output)
  527. output = t.I18nBot("tgbot.commands.pleaseChoose")
  528. t.SendAnswer(chatId, output, false)
  529. }
  530. // searchClientIps searches and sends client IP addresses for the given email.
  531. func (t *Tgbot) searchClientIps(chatId int64, email string, messageID ...int) {
  532. ips, err := t.inboundService.GetInboundClientIps(email)
  533. if err != nil || len(ips) == 0 {
  534. ips = t.I18nBot("tgbot.noIpRecord")
  535. }
  536. formattedIps := ips
  537. if err == nil && len(ips) > 0 {
  538. type ipWithTimestamp struct {
  539. IP string `json:"ip"`
  540. Timestamp int64 `json:"timestamp"`
  541. }
  542. var ipsWithTime []ipWithTimestamp
  543. if json.Unmarshal([]byte(ips), &ipsWithTime) == nil && len(ipsWithTime) > 0 {
  544. lines := make([]string, 0, len(ipsWithTime))
  545. for _, item := range ipsWithTime {
  546. if item.IP == "" {
  547. continue
  548. }
  549. if item.Timestamp > 0 {
  550. ts := time.Unix(item.Timestamp, 0).Format("2006-01-02 15:04:05")
  551. lines = append(lines, fmt.Sprintf("%s (%s)", item.IP, ts))
  552. continue
  553. }
  554. lines = append(lines, item.IP)
  555. }
  556. if len(lines) > 0 {
  557. formattedIps = strings.Join(lines, "\n")
  558. }
  559. } else {
  560. var oldIps []string
  561. if json.Unmarshal([]byte(ips), &oldIps) == nil && len(oldIps) > 0 {
  562. formattedIps = strings.Join(oldIps, "\n")
  563. }
  564. }
  565. }
  566. output := ""
  567. output += t.I18nBot("tgbot.messages.email", "Email=="+email)
  568. output += t.I18nBot("tgbot.messages.ips", "IPs=="+formattedIps)
  569. output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
  570. inlineKeyboard := tu.InlineKeyboard(
  571. tu.InlineKeyboardRow(
  572. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("ips_refresh "+email)),
  573. ),
  574. tu.InlineKeyboardRow(
  575. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clearIPs")).WithCallbackData(t.encodeQuery("clear_ips "+email)),
  576. ),
  577. )
  578. if len(messageID) > 0 {
  579. t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
  580. } else {
  581. t.SendMsgToTgbot(chatId, output, inlineKeyboard)
  582. }
  583. }
  584. // clientTelegramUserInfo retrieves and sends Telegram user info for the client.
  585. func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...int) {
  586. traffic, client, err := t.inboundService.GetClientByEmail(email)
  587. if err != nil {
  588. logger.Warning(err)
  589. msg := t.I18nBot("tgbot.wentWrong")
  590. t.SendMsgToTgbot(chatId, msg)
  591. return
  592. }
  593. if client == nil {
  594. msg := t.I18nBot("tgbot.noResult")
  595. t.SendMsgToTgbot(chatId, msg)
  596. return
  597. }
  598. tgId := "None"
  599. if client.TgID != 0 {
  600. tgId = strconv.FormatInt(client.TgID, 10)
  601. }
  602. output := ""
  603. output += t.I18nBot("tgbot.messages.email", "Email=="+email)
  604. output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId)
  605. output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05"))
  606. inlineKeyboard := tu.InlineKeyboard(
  607. tu.InlineKeyboardRow(
  608. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("tgid_refresh "+email)),
  609. ),
  610. tu.InlineKeyboardRow(
  611. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.removeTGUser")).WithCallbackData(t.encodeQuery("tgid_remove "+email)),
  612. ),
  613. )
  614. if len(messageID) > 0 {
  615. t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
  616. } else {
  617. t.SendMsgToTgbot(chatId, output, inlineKeyboard)
  618. requestUser := telego.KeyboardButtonRequestUsers{
  619. RequestID: int32(traffic.Id),
  620. UserIsBot: new(bool),
  621. }
  622. keyboard := tu.Keyboard(
  623. tu.KeyboardRow(
  624. tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUsers(&requestUser),
  625. ),
  626. tu.KeyboardRow(
  627. tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")),
  628. ),
  629. ).WithIsPersistent().WithResizeKeyboard()
  630. t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.buttons.selectOneTGUser"), keyboard)
  631. }
  632. }
  633. // searchClient searches for a client by email and sends the information.
  634. func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
  635. traffic, err := t.inboundService.GetClientTrafficByEmail(email)
  636. if err != nil {
  637. logger.Warning(err)
  638. msg := t.I18nBot("tgbot.wentWrong")
  639. t.SendMsgToTgbot(chatId, msg)
  640. return
  641. }
  642. if traffic == nil {
  643. msg := t.I18nBot("tgbot.noResult")
  644. t.SendMsgToTgbot(chatId, msg)
  645. return
  646. }
  647. output := t.clientInfoMsg(traffic, true, true, true, true, true, true)
  648. inlineKeyboard := tu.InlineKeyboard(
  649. tu.InlineKeyboardRow(
  650. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("client_refresh "+email)),
  651. ),
  652. tu.InlineKeyboardRow(
  653. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic "+email)),
  654. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData(t.encodeQuery("limit_traffic "+email)),
  655. ),
  656. tu.InlineKeyboardRow(
  657. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData(t.encodeQuery("reset_exp "+email)),
  658. ),
  659. tu.InlineKeyboardRow(
  660. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLog")).WithCallbackData(t.encodeQuery("ip_log "+email)),
  661. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData(t.encodeQuery("ip_limit "+email)),
  662. ),
  663. tu.InlineKeyboardRow(
  664. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.setTGUser")).WithCallbackData(t.encodeQuery("tg_user "+email)),
  665. ),
  666. tu.InlineKeyboardRow(
  667. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.toggle")).WithCallbackData(t.encodeQuery("toggle_enable "+email)),
  668. ),
  669. )
  670. if len(messageID) > 0 {
  671. t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
  672. } else {
  673. t.SendMsgToTgbot(chatId, output, inlineKeyboard)
  674. }
  675. }
  676. // getCommonClientButtons returns the shared inline keyboard rows for the
  677. // client-first multi-inbound add flow. Per-protocol secrets (UUID, password,
  678. // flow, method) are generated by fillProtocolDefaults on submit, so the bot
  679. // only exposes the universal client fields here.
  680. func (t *Tgbot) getCommonClientButtons() [][]telego.InlineKeyboardButton {
  681. attachLabel := fmt.Sprintf("➕ Attach inbound (%d)", len(receiver_inbound_IDs))
  682. return [][]telego.InlineKeyboardButton{
  683. tu.InlineKeyboardRow(
  684. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_email")).WithCallbackData("add_client_ch_default_email"),
  685. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.change_comment")).WithCallbackData("add_client_ch_default_comment"),
  686. ),
  687. tu.InlineKeyboardRow(
  688. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.limitTraffic")).WithCallbackData("add_client_ch_default_traffic"),
  689. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.resetExpire")).WithCallbackData("add_client_ch_default_exp"),
  690. ),
  691. tu.InlineKeyboardRow(
  692. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.ipLimit")).WithCallbackData("add_client_ch_default_ip_limit"),
  693. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.setTGUser")).WithCallbackData("add_client_ch_default_tg_id"),
  694. ),
  695. tu.InlineKeyboardRow(
  696. tu.InlineKeyboardButton(attachLabel).WithCallbackData("add_client_attach_more"),
  697. ),
  698. tu.InlineKeyboardRow(
  699. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitDisable")).WithCallbackData("add_client_submit_disable"),
  700. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.submitEnable")).WithCallbackData("add_client_submit_enable"),
  701. ),
  702. tu.InlineKeyboardRow(
  703. tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.cancel")).WithCallbackData("add_client_cancel"),
  704. ),
  705. }
  706. }
  707. // addClient renders the draft message + shared client-first keyboard.
  708. func (t *Tgbot) addClient(chatId int64, msg string, messageID ...int) {
  709. inlineKeyboard := tu.InlineKeyboard(t.getCommonClientButtons()...)
  710. if len(messageID) > 0 {
  711. t.editMessageTgBot(chatId, messageID[0], msg, inlineKeyboard)
  712. } else {
  713. t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
  714. }
  715. }