telegram_bot.go 35 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010
  1. package server
  2. import (
  3. "bytes"
  4. "context"
  5. cryptorand "crypto/rand"
  6. "encoding/hex"
  7. "encoding/json"
  8. "errors"
  9. "fmt"
  10. "io"
  11. "net/http"
  12. "net/http/httptest"
  13. "strconv"
  14. "strings"
  15. "sync"
  16. "time"
  17. "vocat/internal/device"
  18. "vocat/internal/modem"
  19. "vocat/internal/store"
  20. "vocat/internal/vowifi"
  21. vowifiruntime "vocat/internal/vowifi/runtime"
  22. )
  23. const (
  24. telegramPollInterval = 3 * time.Second
  25. telegramNotificationPeriod = 2 * time.Second
  26. telegramConfirmationTTL = 2 * time.Minute
  27. telegramMaxDialDuration = 10 * time.Minute
  28. )
  29. type telegramRuntimeConfig struct {
  30. Token string
  31. ChatID string
  32. AdminID int64
  33. BaseURL string
  34. Proxy string
  35. }
  36. type telegramBot struct {
  37. server *Server
  38. pendingMu sync.Mutex
  39. pending map[string]telegramPendingAction
  40. logMu sync.Mutex
  41. lastLogTime time.Time
  42. lastLogText string
  43. }
  44. type telegramPendingAction struct {
  45. Kind string
  46. DeviceID string
  47. Argument string
  48. Text string
  49. Duration time.Duration
  50. ChatID int64
  51. AdminID int64
  52. CreatedAt time.Time
  53. TargetAID string
  54. TargetICCID string
  55. }
  56. type telegramAPIResponse struct {
  57. OK bool `json:"ok"`
  58. Description string `json:"description"`
  59. Result json.RawMessage `json:"result"`
  60. }
  61. type telegramUpdate struct {
  62. UpdateID int64 `json:"update_id"`
  63. Message *telegramMessage `json:"message"`
  64. CallbackQuery *telegramCallbackQuery `json:"callback_query"`
  65. }
  66. type telegramMessage struct {
  67. MessageID int64 `json:"message_id"`
  68. From *telegramUser `json:"from"`
  69. Chat telegramChat `json:"chat"`
  70. Text string `json:"text"`
  71. }
  72. type telegramUser struct {
  73. ID int64 `json:"id"`
  74. }
  75. type telegramChat struct {
  76. ID int64 `json:"id"`
  77. }
  78. type telegramCallbackQuery struct {
  79. ID string `json:"id"`
  80. From telegramUser `json:"from"`
  81. Message *telegramMessage `json:"message"`
  82. Data string `json:"data"`
  83. }
  84. // StartTelegramBot starts both the Telegram command poller and durable inbound
  85. // SMS notifier. Configuration is reloaded between polls, so saving Settings
  86. // takes effect without restarting vocat.
  87. func (s *Server) StartTelegramBot(ctx context.Context) {
  88. if ctx == nil {
  89. ctx = context.Background()
  90. }
  91. bot := &telegramBot{
  92. server: s,
  93. pending: make(map[string]telegramPendingAction),
  94. }
  95. go bot.poll(ctx)
  96. go bot.notifyInboundSMS(ctx)
  97. }
  98. func (bot *telegramBot) poll(ctx context.Context) {
  99. activeToken := ""
  100. var offset int64
  101. for ctx.Err() == nil {
  102. config, enabled, err := bot.loadConfig(ctx)
  103. if err != nil {
  104. bot.warn("load Telegram bot configuration", err)
  105. if !waitTelegram(ctx, telegramPollInterval) {
  106. return
  107. }
  108. continue
  109. }
  110. if !enabled {
  111. activeToken = ""
  112. offset = 0
  113. if !waitTelegram(ctx, telegramPollInterval) {
  114. return
  115. }
  116. continue
  117. }
  118. if config.Token != activeToken {
  119. offset, err = bot.bootstrap(ctx, config)
  120. if err != nil {
  121. bot.warn("start Telegram bot polling", err)
  122. if !waitTelegram(ctx, telegramPollInterval) {
  123. return
  124. }
  125. continue
  126. }
  127. activeToken = config.Token
  128. }
  129. pollContext, cancel := context.WithTimeout(ctx, 10*time.Second)
  130. updates, pollErr := bot.getUpdates(pollContext, config, offset, 5)
  131. cancel()
  132. if pollErr != nil {
  133. bot.warn("poll Telegram updates", pollErr)
  134. if !waitTelegram(ctx, telegramPollInterval) {
  135. return
  136. }
  137. continue
  138. }
  139. for _, update := range updates {
  140. if update.UpdateID >= offset {
  141. offset = update.UpdateID + 1
  142. }
  143. update := update
  144. go bot.handleUpdate(ctx, config, update)
  145. }
  146. }
  147. }
  148. // bootstrap discards stale Telegram updates. Replaying an old /sms, /call, or
  149. // /switch command after a service restart would be unsafe even though each
  150. // command has its own confirmation step.
  151. func (bot *telegramBot) bootstrap(ctx context.Context, config telegramRuntimeConfig) (int64, error) {
  152. requestContext, cancel := context.WithTimeout(ctx, 8*time.Second)
  153. defer cancel()
  154. updates, err := bot.getUpdates(requestContext, config, -1, 0)
  155. if err != nil {
  156. return 0, err
  157. }
  158. var offset int64
  159. for _, update := range updates {
  160. if update.UpdateID >= offset {
  161. offset = update.UpdateID + 1
  162. }
  163. }
  164. commands := []map[string]string{
  165. {"command": "status", "description": "查看设备状态"},
  166. {"command": "esim", "description": "查看已安装 eSIM Profile"},
  167. {"command": "wfc", "description": "管理 WiFi Calling"},
  168. {"command": "sms", "description": "发送短信(需要确认)"},
  169. {"command": "call", "description": "限时拨号并自动挂断(需要确认)"},
  170. {"command": "calls", "description": "查看当前通话"},
  171. {"command": "hangup", "description": "挂断通话"},
  172. {"command": "help", "description": "查看命令帮助"},
  173. }
  174. _ = bot.call(requestContext, config, "setMyCommands", map[string]any{"commands": commands}, nil)
  175. return offset, nil
  176. }
  177. func (bot *telegramBot) getUpdates(
  178. ctx context.Context,
  179. config telegramRuntimeConfig,
  180. offset int64,
  181. timeout int,
  182. ) ([]telegramUpdate, error) {
  183. payload := map[string]any{
  184. "offset": offset,
  185. "timeout": timeout,
  186. "allowed_updates": []string{"message", "callback_query"},
  187. }
  188. var updates []telegramUpdate
  189. if err := bot.call(ctx, config, "getUpdates", payload, &updates); err != nil {
  190. return nil, err
  191. }
  192. return updates, nil
  193. }
  194. func (bot *telegramBot) handleUpdate(ctx context.Context, config telegramRuntimeConfig, update telegramUpdate) {
  195. if callback := update.CallbackQuery; callback != nil {
  196. if callback.Message == nil || !bot.authorized(config, callback.Message.Chat.ID, callback.From.ID) {
  197. _ = bot.answerCallback(ctx, config, callback.ID, "无权限")
  198. return
  199. }
  200. _ = bot.answerCallback(ctx, config, callback.ID, "")
  201. bot.handleCallback(ctx, config, callback)
  202. return
  203. }
  204. message := update.Message
  205. if message == nil || message.From == nil || !bot.authorized(config, message.Chat.ID, message.From.ID) {
  206. return
  207. }
  208. command, remainder := parseTelegramCommand(message.Text)
  209. if command == "" {
  210. return
  211. }
  212. switch command {
  213. case "start", "menu", "help":
  214. bot.sendHelp(ctx, config, message.Chat.ID)
  215. case "status", "devices":
  216. bot.sendDeviceStatus(ctx, config, message.Chat.ID, strings.TrimSpace(remainder))
  217. case "esim":
  218. bot.sendESIMProfiles(ctx, config, message.Chat.ID, strings.TrimSpace(remainder))
  219. case "switch":
  220. parts := strings.Fields(remainder)
  221. if len(parts) != 2 {
  222. bot.sendText(ctx, config, message.Chat.ID, "用法:/switch <设备ID> <目标ICCID>", nil)
  223. return
  224. }
  225. bot.confirmESIMSwitch(ctx, config, message.Chat.ID, message.From.ID, parts[0], parts[1])
  226. case "wfc", "wificalling":
  227. parts := strings.Fields(remainder)
  228. if len(parts) != 2 {
  229. bot.sendText(ctx, config, message.Chat.ID, "用法:/wfc <设备ID> <status|on|off|reconnect>", nil)
  230. return
  231. }
  232. bot.handleVoWiFi(ctx, config, message.Chat.ID, message.From.ID, parts[0], parts[1])
  233. case "sms":
  234. parts := splitTelegramArguments(remainder, 3)
  235. if len(parts) != 3 {
  236. bot.sendText(ctx, config, message.Chat.ID, "用法:/sms <设备ID> <号码> <短信内容>", nil)
  237. return
  238. }
  239. bot.confirmSMS(ctx, config, message.Chat.ID, message.From.ID, parts[0], parts[1], parts[2])
  240. case "call":
  241. parts := strings.Fields(remainder)
  242. if len(parts) != 3 {
  243. bot.sendText(ctx, config, message.Chat.ID, "用法:/call <设备ID> <号码> <持续秒数>\n拨号后将在指定时间自动挂断,不处理通话音频。", nil)
  244. return
  245. }
  246. seconds, err := strconv.Atoi(parts[2])
  247. if err != nil || seconds < 1 || time.Duration(seconds)*time.Second > telegramMaxDialDuration {
  248. bot.sendText(ctx, config, message.Chat.ID, "持续时间必须是 1–600 秒。", nil)
  249. return
  250. }
  251. bot.confirmCall(ctx, config, message.Chat.ID, message.From.ID, parts[0], parts[1], time.Duration(seconds)*time.Second)
  252. case "answer":
  253. bot.executeSimpleCallAction(ctx, config, message.Chat.ID, message.From.ID, strings.TrimSpace(remainder), "answer")
  254. case "hangup":
  255. bot.executeSimpleCallAction(ctx, config, message.Chat.ID, message.From.ID, strings.TrimSpace(remainder), "hangup")
  256. case "calls":
  257. bot.executeSimpleCallAction(ctx, config, message.Chat.ID, message.From.ID, strings.TrimSpace(remainder), "status")
  258. default:
  259. bot.sendText(ctx, config, message.Chat.ID, "未知命令。发送 /help 查看可用操作。", nil)
  260. }
  261. }
  262. func (bot *telegramBot) handleCallback(ctx context.Context, config telegramRuntimeConfig, callback *telegramCallbackQuery) {
  263. data := strings.TrimSpace(callback.Data)
  264. if data == "menu:status" {
  265. bot.sendDeviceStatus(ctx, config, callback.Message.Chat.ID, "")
  266. return
  267. }
  268. if data == "menu:help" {
  269. bot.sendHelp(ctx, config, callback.Message.Chat.ID)
  270. return
  271. }
  272. decision, token, found := strings.Cut(data, ":")
  273. if !found || (decision != "confirm" && decision != "cancel") {
  274. return
  275. }
  276. action, ok := bot.takePending(token, callback.Message.Chat.ID, callback.From.ID)
  277. if !ok {
  278. bot.sendText(ctx, config, callback.Message.Chat.ID, "该确认已过期或已处理。", nil)
  279. return
  280. }
  281. if decision == "cancel" {
  282. bot.sendText(ctx, config, callback.Message.Chat.ID, "操作已取消。", nil)
  283. return
  284. }
  285. switch action.Kind {
  286. case "sms":
  287. bot.sendText(ctx, config, action.ChatID, "正在提交短信…", nil)
  288. result, err := bot.executeSMS(ctx, action)
  289. bot.finishAction(ctx, config, action, "telegram.sms.send", result, err)
  290. case "esim_switch":
  291. bot.sendText(ctx, config, action.ChatID, "正在切换 Profile 并等待模块恢复校验…", nil)
  292. result, err := bot.executeESIMSwitch(ctx, action)
  293. bot.finishAction(ctx, config, action, "telegram.esim.switch", result, err)
  294. case "call":
  295. result, err := bot.executeTimedCall(ctx, config, action)
  296. bot.finishAction(ctx, config, action, "telegram.call.dial", result, err)
  297. }
  298. }
  299. func (bot *telegramBot) sendHelp(ctx context.Context, config telegramRuntimeConfig, chatID int64) {
  300. text := strings.Join([]string{
  301. "vocat Telegram 控制", "",
  302. "/status [设备ID] — 查看设备、SIM、蜂窝与 VoWiFi 状态",
  303. "/esim <设备ID> — 只读查看已安装 Profile",
  304. "/switch <设备ID> <ICCID> — 切换到已安装 Profile(需确认)",
  305. "/wfc <设备ID> <status|on|off|reconnect> — 管理 WiFi Calling",
  306. "/sms <设备ID> <号码> <内容> — 发送短信(需确认)",
  307. "/call <设备ID> <号码> <秒数> — 拨号并在 1–600 秒后自动挂断(需确认)",
  308. "/calls <设备ID> — 查看模块当前通话",
  309. "/answer <设备ID> — 接听蜂窝来电",
  310. "/hangup <设备ID> — 立即挂断",
  311. "",
  312. "Bot 不提供 eSIM 下载、删除或改名,也不采集或转发通话音频。控制命令只接受设置中的 Admin ID。",
  313. }, "\n")
  314. keyboard := map[string]any{"inline_keyboard": [][]map[string]string{{
  315. {"text": "📊 设备状态", "callback_data": "menu:status"},
  316. {"text": "❓ 帮助", "callback_data": "menu:help"},
  317. }}}
  318. bot.sendText(ctx, config, chatID, text, keyboard)
  319. }
  320. func (bot *telegramBot) sendDeviceStatus(ctx context.Context, config telegramRuntimeConfig, chatID int64, onlyID string) {
  321. configs, err := bot.server.store.ListDevices(ctx)
  322. if err != nil {
  323. bot.sendText(ctx, config, chatID, "读取设备失败:"+err.Error(), nil)
  324. return
  325. }
  326. var blocks []string
  327. for _, stored := range configs {
  328. if onlyID != "" && stored.ID != onlyID {
  329. continue
  330. }
  331. entry, _, present := bot.server.physicalForConfig(stored)
  332. lines := []string{fmt.Sprintf("📡 %s (%s)", firstNonEmpty(stored.Name, stored.ID), stored.ID)}
  333. if !present {
  334. lines = append(lines, "设备:离线")
  335. } else {
  336. lines = append(lines, "设备:在线")
  337. if snapshot := entry.Snapshot; snapshot != nil {
  338. lines = append(lines,
  339. "SIM:"+map[bool]string{true: "Ready", false: firstNonEmpty(snapshot.SIMStatus, "未就绪")}[snapshot.SIMReady],
  340. "ICCID:"+firstNonEmpty(snapshot.ICCID, "--"),
  341. "IMSI:"+firstNonEmpty(snapshot.IMSI, "--"),
  342. "号码:"+firstNonEmpty(snapshot.Phone.Number, "--"),
  343. "运营商:"+firstNonEmpty(snapshot.OperatorName, snapshot.OperatorCode, "--"),
  344. "蜂窝模式:"+map[bool]string{true: "飞行模式", false: "开启"}[snapshot.FlightMode],
  345. )
  346. }
  347. }
  348. if bot.server.vowifi != nil {
  349. if state, stateErr := bot.server.vowifi.State(stored.ID); stateErr == nil {
  350. lines = append(lines,
  351. fmt.Sprintf("VoWiFi:%s · Tunnel=%t IMS=%t SMS=%t", firstNonEmpty(string(state.Phase), "idle"), state.TunnelReady, state.IMSReady, state.SMSReady),
  352. )
  353. if state.LastError != "" {
  354. lines = append(lines, "最后错误:"+state.LastError)
  355. }
  356. }
  357. }
  358. blocks = append(blocks, strings.Join(lines, "\n"))
  359. }
  360. if len(blocks) == 0 {
  361. bot.sendText(ctx, config, chatID, "未找到设备 "+onlyID, nil)
  362. return
  363. }
  364. bot.sendText(ctx, config, chatID, strings.Join(blocks, "\n\n"), nil)
  365. }
  366. func (bot *telegramBot) sendESIMProfiles(ctx context.Context, config telegramRuntimeConfig, chatID int64, deviceID string) {
  367. if deviceID == "" {
  368. bot.sendText(ctx, config, chatID, "用法:/esim <设备ID>", nil)
  369. return
  370. }
  371. _, _, physicalID, err := bot.device(deviceID)
  372. if err != nil {
  373. bot.sendText(ctx, config, chatID, "读取 eSIM 失败:"+err.Error(), nil)
  374. return
  375. }
  376. readContext, cancel := context.WithTimeout(ctx, 30*time.Second)
  377. defer cancel()
  378. inventory, err := bot.server.devices.ESIMInventory(readContext, physicalID)
  379. if err != nil {
  380. bot.sendText(ctx, config, chatID, "读取 eSIM 失败:"+err.Error(), nil)
  381. return
  382. }
  383. if len(inventory) == 0 {
  384. bot.sendText(ctx, config, chatID, "该设备没有可用的 eUICC/Profile。", nil)
  385. return
  386. }
  387. lines := []string{"📲 " + deviceID + " 已安装 Profile(只读)"}
  388. for index, group := range inventory {
  389. lines = append(lines, fmt.Sprintf("\neUICC #%d · …%s", index+1, tailDigits(group.Info.EID, 4)))
  390. for _, profile := range group.Info.Profiles {
  391. state := "Disabled"
  392. if profile.State == 1 {
  393. state = "Enabled"
  394. }
  395. name := firstNonEmpty(profile.Nickname, profile.Name, profile.ServiceProvider, "未命名")
  396. lines = append(lines, fmt.Sprintf("• %s · %s\n %s", name, state, profile.ICCID))
  397. }
  398. }
  399. lines = append(lines, "\n切换:/switch "+deviceID+" <目标ICCID>")
  400. bot.sendText(ctx, config, chatID, strings.Join(lines, "\n"), nil)
  401. }
  402. func (bot *telegramBot) confirmESIMSwitch(ctx context.Context, config telegramRuntimeConfig, chatID, adminID int64, deviceID, iccid string) {
  403. _, _, physicalID, err := bot.device(deviceID)
  404. if err != nil {
  405. bot.sendText(ctx, config, chatID, "无法切换:"+err.Error(), nil)
  406. return
  407. }
  408. readContext, cancel := context.WithTimeout(ctx, 30*time.Second)
  409. defer cancel()
  410. inventory, err := bot.server.devices.ESIMInventory(readContext, physicalID)
  411. if err != nil {
  412. bot.sendText(ctx, config, chatID, "无法读取 Profile:"+err.Error(), nil)
  413. return
  414. }
  415. var target *device.EsimProfile
  416. var targetAID string
  417. for groupIndex := range inventory {
  418. for profileIndex := range inventory[groupIndex].Info.Profiles {
  419. profile := &inventory[groupIndex].Info.Profiles[profileIndex]
  420. if profile.ICCID == iccid {
  421. target = profile
  422. targetAID = inventory[groupIndex].Info.AID
  423. break
  424. }
  425. }
  426. }
  427. if target == nil {
  428. bot.sendText(ctx, config, chatID, "目标 ICCID 不在该设备已安装 Profile 中。", nil)
  429. return
  430. }
  431. if target.State == 1 {
  432. bot.sendText(ctx, config, chatID, "目标 Profile 已经处于 Enabled。", nil)
  433. return
  434. }
  435. action := telegramPendingAction{
  436. Kind: "esim_switch", DeviceID: deviceID, ChatID: chatID, AdminID: adminID,
  437. CreatedAt: time.Now(), TargetAID: targetAID, TargetICCID: target.ICCID,
  438. }
  439. name := firstNonEmpty(target.Nickname, target.Name, target.ServiceProvider, "未命名")
  440. bot.askConfirmation(ctx, config, action, fmt.Sprintf("确认将设备 %s 切换到:\n%s\nICCID %s?\n\nBot 只会执行 EnableProfile,不会下载或删除 Profile。", deviceID, name, target.ICCID))
  441. }
  442. func (bot *telegramBot) confirmSMS(ctx context.Context, config telegramRuntimeConfig, chatID, adminID int64, deviceID, phone, text string) {
  443. phone = strings.TrimSpace(phone)
  444. text = strings.TrimSpace(text)
  445. if _, _, _, err := bot.device(deviceID); err != nil {
  446. bot.sendText(ctx, config, chatID, "无法发送:"+err.Error(), nil)
  447. return
  448. }
  449. if blocked, reason := blockedSMSDestination(phone); blocked {
  450. bot.sendText(ctx, config, chatID, "无法发送:"+reason, nil)
  451. return
  452. }
  453. if text == "" {
  454. bot.sendText(ctx, config, chatID, "短信内容不能为空。", nil)
  455. return
  456. }
  457. action := telegramPendingAction{
  458. Kind: "sms", DeviceID: deviceID, Argument: phone, Text: text,
  459. ChatID: chatID, AdminID: adminID, CreatedAt: time.Now(),
  460. }
  461. bot.askConfirmation(ctx, config, action, fmt.Sprintf("确认通过设备 %s 发送短信?\n收件人:%s\n内容:%s", deviceID, phone, truncateTelegramText(text, 800)))
  462. }
  463. func (bot *telegramBot) confirmCall(ctx context.Context, config telegramRuntimeConfig, chatID, adminID int64, deviceID, number string, duration time.Duration) {
  464. if !validTelegramDialNumber(number) {
  465. bot.sendText(ctx, config, chatID, "拨号号码无效,只允许一个可选的前导 + 和 3–20 位数字。", nil)
  466. return
  467. }
  468. if _, entry, _, err := bot.device(deviceID); err != nil {
  469. bot.sendText(ctx, config, chatID, "无法拨号:"+err.Error(), nil)
  470. return
  471. } else if entry.Snapshot != nil && entry.Snapshot.FlightMode {
  472. bot.sendText(ctx, config, chatID, "设备处于飞行模式,蜂窝语音拨号不可用。当前 Bot 不实现 IMS 语音或音频处理。", nil)
  473. return
  474. }
  475. action := telegramPendingAction{
  476. Kind: "call", DeviceID: deviceID, Argument: number, Duration: duration,
  477. ChatID: chatID, AdminID: adminID, CreatedAt: time.Now(),
  478. }
  479. bot.askConfirmation(ctx, config, action, fmt.Sprintf("确认通过设备 %s 拨打 %s?\n持续:%d 秒,然后自动挂断。\n不会采集或处理通话音频。", deviceID, number, int(duration/time.Second)))
  480. }
  481. func (bot *telegramBot) askConfirmation(ctx context.Context, config telegramRuntimeConfig, action telegramPendingAction, text string) {
  482. token, err := bot.putPending(action)
  483. if err != nil {
  484. bot.sendText(ctx, config, action.ChatID, "创建确认失败:"+err.Error(), nil)
  485. return
  486. }
  487. keyboard := map[string]any{"inline_keyboard": [][]map[string]string{{
  488. {"text": "✅ 确认", "callback_data": "confirm:" + token},
  489. {"text": "❌ 取消", "callback_data": "cancel:" + token},
  490. }}}
  491. bot.sendText(ctx, config, action.ChatID, text, keyboard)
  492. }
  493. func (bot *telegramBot) putPending(action telegramPendingAction) (string, error) {
  494. raw := make([]byte, 8)
  495. if _, err := cryptorand.Read(raw); err != nil {
  496. return "", err
  497. }
  498. token := hex.EncodeToString(raw)
  499. bot.pendingMu.Lock()
  500. defer bot.pendingMu.Unlock()
  501. now := time.Now()
  502. for key, value := range bot.pending {
  503. if now.Sub(value.CreatedAt) > telegramConfirmationTTL {
  504. delete(bot.pending, key)
  505. }
  506. }
  507. bot.pending[token] = action
  508. return token, nil
  509. }
  510. func (bot *telegramBot) takePending(token string, chatID, adminID int64) (telegramPendingAction, bool) {
  511. bot.pendingMu.Lock()
  512. defer bot.pendingMu.Unlock()
  513. action, ok := bot.pending[token]
  514. if ok {
  515. delete(bot.pending, token)
  516. }
  517. if !ok || action.ChatID != chatID || action.AdminID != adminID || time.Since(action.CreatedAt) > telegramConfirmationTTL {
  518. return telegramPendingAction{}, false
  519. }
  520. return action, true
  521. }
  522. func (bot *telegramBot) executeSMS(ctx context.Context, action telegramPendingAction) (string, error) {
  523. payload, _ := json.Marshal(map[string]string{
  524. "device_id": action.DeviceID,
  525. "phone": action.Argument,
  526. "message": action.Text,
  527. })
  528. request := httptest.NewRequest(http.MethodPost, "/api/sms/send", bytes.NewReader(payload)).WithContext(ctx)
  529. request.Header.Set("Content-Type", "application/json")
  530. recorder := httptest.NewRecorder()
  531. bot.server.handleSMSSend(recorder, request)
  532. var response struct {
  533. Data map[string]any `json:"data"`
  534. Error *apiError `json:"error"`
  535. }
  536. if err := json.Unmarshal(recorder.Body.Bytes(), &response); err != nil {
  537. return "", fmt.Errorf("decode SMS result: %w", err)
  538. }
  539. if recorder.Code >= http.StatusBadRequest || response.Error != nil {
  540. if response.Error != nil {
  541. return "", errors.New(response.Error.Message)
  542. }
  543. return "", fmt.Errorf("SMS submission returned HTTP %d", recorder.Code)
  544. }
  545. return fmt.Sprintf("短信已提交。\n通道:%v\n结果:%v\n送达确认:%v", response.Data["transport"], response.Data["outcome"], response.Data["delivery_confirmed"]), nil
  546. }
  547. func (bot *telegramBot) executeESIMSwitch(ctx context.Context, action telegramPendingAction) (string, error) {
  548. _, _, physicalID, err := bot.device(action.DeviceID)
  549. if err != nil {
  550. return "", err
  551. }
  552. operationContext, cancel := context.WithTimeout(ctx, 2*time.Minute)
  553. defer cancel()
  554. if err := bot.server.devices.ESIMSwitchProfile(operationContext, physicalID, action.TargetICCID, action.TargetAID); err != nil {
  555. return "", err
  556. }
  557. return "Profile 切换成功,模块恢复后已校验当前 ICCID:" + action.TargetICCID, nil
  558. }
  559. func (bot *telegramBot) executeTimedCall(ctx context.Context, config telegramRuntimeConfig, action telegramPendingAction) (string, error) {
  560. _, entry, physicalID, err := bot.device(action.DeviceID)
  561. if err != nil {
  562. return "", err
  563. }
  564. if entry.Snapshot != nil && entry.Snapshot.FlightMode {
  565. return "", errors.New("device is in airplane mode")
  566. }
  567. dialContext, cancelDial := context.WithTimeout(ctx, 20*time.Second)
  568. response, err := bot.server.devices.ExecuteAT(dialContext, physicalID, "ATD"+action.Argument+";")
  569. cancelDial()
  570. if err != nil {
  571. return "", fmt.Errorf("拨号失败: %w", err)
  572. }
  573. if !strings.EqualFold(strings.TrimSpace(response.Final), "OK") {
  574. return "", fmt.Errorf("拨号未被模块接受: %s", formatTelegramAT(response))
  575. }
  576. bot.sendText(ctx, config, action.ChatID, fmt.Sprintf("📞 已开始拨打 %s,将在 %d 秒后自动挂断。", action.Argument, int(action.Duration/time.Second)), nil)
  577. timer := time.NewTimer(action.Duration)
  578. defer timer.Stop()
  579. select {
  580. case <-ctx.Done():
  581. return "", ctx.Err()
  582. case <-timer.C:
  583. }
  584. hangContext, cancelHang := context.WithTimeout(context.Background(), 15*time.Second)
  585. defer cancelHang()
  586. hangResponse, hangErr := bot.server.devices.ExecuteAT(hangContext, physicalID, "ATH")
  587. if hangErr != nil {
  588. return "", fmt.Errorf("拨号已执行,但自动挂断失败: %w", hangErr)
  589. }
  590. if !strings.EqualFold(strings.TrimSpace(hangResponse.Final), "OK") {
  591. return "", fmt.Errorf("拨号已执行,但模块未确认自动挂断: %s", formatTelegramAT(hangResponse))
  592. }
  593. return fmt.Sprintf("拨号动作完成:%s,持续 %d 秒后已自动挂断。", action.Argument, int(action.Duration/time.Second)), nil
  594. }
  595. func (bot *telegramBot) executeSimpleCallAction(ctx context.Context, config telegramRuntimeConfig, chatID, adminID int64, deviceID, action string) {
  596. if deviceID == "" {
  597. bot.sendText(ctx, config, chatID, fmt.Sprintf("用法:/%s <设备ID>", map[string]string{"status": "calls", "answer": "answer", "hangup": "hangup"}[action]), nil)
  598. return
  599. }
  600. _, _, physicalID, err := bot.device(deviceID)
  601. if err != nil {
  602. bot.sendText(ctx, config, chatID, "通话操作失败:"+err.Error(), nil)
  603. return
  604. }
  605. command := map[string]string{"status": "AT+CLCC", "answer": "ATA", "hangup": "ATH"}[action]
  606. operationContext, cancel := context.WithTimeout(ctx, 20*time.Second)
  607. response, err := bot.server.devices.ExecuteAT(operationContext, physicalID, command)
  608. cancel()
  609. outcome := "success"
  610. if err != nil {
  611. outcome = "failure"
  612. bot.sendText(ctx, config, chatID, "通话操作失败:"+err.Error(), nil)
  613. } else {
  614. text := formatTelegramAT(response)
  615. if action == "status" && strings.TrimSpace(response.Text()) == "" {
  616. text = "当前没有活动通话。"
  617. }
  618. bot.sendText(ctx, config, chatID, text, nil)
  619. }
  620. bot.server.recordAudit(ctx, fmt.Sprintf("telegram:%d", adminID), "telegram.call."+action, "device", deviceID, outcome, "telegram")
  621. }
  622. func (bot *telegramBot) handleVoWiFi(ctx context.Context, config telegramRuntimeConfig, chatID, adminID int64, deviceID, operation string) {
  623. stored, entry, _, err := bot.device(deviceID)
  624. if err != nil {
  625. bot.sendText(ctx, config, chatID, "VoWiFi 操作失败:"+err.Error(), nil)
  626. return
  627. }
  628. if bot.server.vowifi == nil {
  629. bot.sendText(ctx, config, chatID, "VoWiFi runtime 不可用。", nil)
  630. return
  631. }
  632. operation = strings.ToLower(strings.TrimSpace(operation))
  633. if operation == "status" {
  634. state, stateErr := bot.server.vowifi.State(deviceID)
  635. if stateErr != nil {
  636. bot.sendText(ctx, config, chatID, "读取 VoWiFi 状态失败:"+stateErr.Error(), nil)
  637. return
  638. }
  639. bot.sendText(ctx, config, chatID, formatTelegramVoWiFiState(state), nil)
  640. return
  641. }
  642. var state vowifi.State
  643. switch operation {
  644. case "on", "off":
  645. enabled := operation == "on"
  646. if enabled && entry.Snapshot != nil {
  647. if reason := device.RegionBlockReason(entry.Snapshot.IMSI); reason != "" {
  648. bot.sendText(ctx, config, chatID, "VoWiFi 操作被拒绝:"+reason, nil)
  649. return
  650. }
  651. }
  652. previous := stored.VoWiFiEnabled
  653. stored.VoWiFiEnabled = enabled
  654. if err = bot.server.store.UpsertDevice(ctx, stored); err == nil {
  655. state, err = bot.server.vowifi.RequestEnabled(deviceID, enabled)
  656. }
  657. if err != nil {
  658. stored.VoWiFiEnabled = previous
  659. _ = bot.server.store.UpsertDevice(ctx, stored)
  660. if errors.Is(err, vowifiruntime.ErrOperationInProgress) && state.Enabled == enabled {
  661. err = nil
  662. }
  663. }
  664. case "reconnect":
  665. if !stored.VoWiFiEnabled {
  666. err = errors.New("请先启用 VoWiFi")
  667. } else {
  668. state, err = bot.server.vowifi.RequestReconnect(deviceID)
  669. }
  670. default:
  671. bot.sendText(ctx, config, chatID, "操作必须是 status、on、off 或 reconnect。", nil)
  672. return
  673. }
  674. outcome := "success"
  675. if err != nil {
  676. outcome = "failure"
  677. bot.sendText(ctx, config, chatID, "VoWiFi 操作失败:"+err.Error(), nil)
  678. } else {
  679. bot.sendText(ctx, config, chatID, "VoWiFi 操作已受理。\n"+formatTelegramVoWiFiState(state), nil)
  680. }
  681. bot.server.recordAudit(ctx, fmt.Sprintf("telegram:%d", adminID), "telegram.vowifi."+operation, "device", deviceID, outcome, "telegram")
  682. }
  683. func (bot *telegramBot) finishAction(ctx context.Context, config telegramRuntimeConfig, action telegramPendingAction, auditAction, result string, err error) {
  684. outcome := "success"
  685. if err != nil {
  686. outcome = "failure"
  687. bot.sendText(ctx, config, action.ChatID, "操作失败:"+err.Error(), nil)
  688. } else {
  689. bot.sendText(ctx, config, action.ChatID, "✅ "+result, nil)
  690. }
  691. bot.server.recordAudit(ctx, fmt.Sprintf("telegram:%d", action.AdminID), auditAction, "device", action.DeviceID, outcome, "telegram")
  692. }
  693. func (bot *telegramBot) notifyInboundSMS(ctx context.Context) {
  694. cursorInitialized := false
  695. var cursor int64
  696. for ctx.Err() == nil {
  697. if !cursorInitialized {
  698. latest, err := bot.server.store.LatestSMSMessageID(ctx)
  699. if err != nil {
  700. bot.warn("initialize Telegram SMS cursor", err)
  701. if !waitTelegram(ctx, telegramNotificationPeriod) {
  702. return
  703. }
  704. continue
  705. }
  706. cursor, cursorInitialized = latest, true
  707. }
  708. config, enabled, err := bot.loadConfig(ctx)
  709. if err != nil {
  710. bot.warn("load Telegram SMS notification configuration", err)
  711. } else if !enabled {
  712. if latest, latestErr := bot.server.store.LatestSMSMessageID(ctx); latestErr == nil {
  713. cursor = latest
  714. }
  715. } else {
  716. messages, listErr := bot.server.store.ListInboundSMSAfterID(ctx, cursor, 100)
  717. if listErr != nil {
  718. bot.warn("list Telegram SMS notifications", listErr)
  719. } else {
  720. for _, message := range messages {
  721. text := fmt.Sprintf("📩 新短信\n设备:%s\n来自:%s\n时间:%s\n\n%s", message.DeviceID, message.Peer, message.Timestamp.Local().Format("2006-01-02 15:04:05"), message.Body)
  722. if sendErr := bot.sendText(ctx, config, 0, text, nil); sendErr != nil {
  723. bot.warn("send Telegram SMS notification", sendErr)
  724. break
  725. }
  726. cursor = message.ID
  727. }
  728. }
  729. }
  730. if !waitTelegram(ctx, telegramNotificationPeriod) {
  731. return
  732. }
  733. }
  734. }
  735. func (bot *telegramBot) device(deviceID string) (store.Device, device.Device, string, error) {
  736. deviceID = strings.TrimSpace(deviceID)
  737. if deviceID == "" {
  738. return store.Device{}, device.Device{}, "", errors.New("设备 ID 不能为空")
  739. }
  740. stored, err := bot.server.store.Device(context.Background(), deviceID)
  741. if err != nil {
  742. return store.Device{}, device.Device{}, "", err
  743. }
  744. entry, physicalID, present := bot.server.physicalForConfig(stored)
  745. if !present {
  746. return stored, entry, "", errors.New("设备不在线")
  747. }
  748. return stored, entry, physicalID, nil
  749. }
  750. func (bot *telegramBot) authorized(config telegramRuntimeConfig, chatID, userID int64) bool {
  751. return config.AdminID > 0 && userID == config.AdminID && strconv.FormatInt(chatID, 10) == config.ChatID
  752. }
  753. func (bot *telegramBot) loadConfig(ctx context.Context) (telegramRuntimeConfig, bool, error) {
  754. setting, err := bot.server.store.NotificationSetting(ctx, "telegram")
  755. if errors.Is(err, store.ErrNotFound) {
  756. return telegramRuntimeConfig{}, false, nil
  757. }
  758. if err != nil {
  759. return telegramRuntimeConfig{}, false, err
  760. }
  761. if !setting.Enabled {
  762. return telegramRuntimeConfig{}, false, nil
  763. }
  764. var raw map[string]any
  765. if err := json.Unmarshal(setting.Config, &raw); err != nil {
  766. return telegramRuntimeConfig{}, false, fmt.Errorf("decode Telegram config: %w", err)
  767. }
  768. config := telegramRuntimeConfig{
  769. Token: configString(raw, "bot_token"),
  770. ChatID: configString(raw, "chat_id"),
  771. BaseURL: configString(raw, "base_url"),
  772. Proxy: configString(raw, "proxy"),
  773. }
  774. if config.BaseURL == "" {
  775. config.BaseURL = "https://api.telegram.org"
  776. }
  777. if admin := configString(raw, "admin_id"); admin != "" {
  778. config.AdminID, err = strconv.ParseInt(admin, 10, 64)
  779. if err != nil || config.AdminID <= 0 {
  780. return telegramRuntimeConfig{}, false, errors.New("telegram.admin_id must be a positive integer")
  781. }
  782. }
  783. if !telegramTokenPattern.MatchString(config.Token) || config.ChatID == "" {
  784. return telegramRuntimeConfig{}, false, errors.New("Telegram bot token or chat id is invalid")
  785. }
  786. return config, true, nil
  787. }
  788. func (bot *telegramBot) call(ctx context.Context, config telegramRuntimeConfig, method string, payload any, result any) error {
  789. base, err := validateOutboundURL(ctx, config.BaseURL, true)
  790. if err != nil {
  791. return err
  792. }
  793. base.Path = strings.TrimRight(base.Path, "/") + "/bot" + config.Token + "/" + method
  794. base.RawPath, base.RawQuery, base.Fragment = "", "", ""
  795. body, err := json.Marshal(payload)
  796. if err != nil {
  797. return err
  798. }
  799. client, err := restrictedHTTPClient(ctx, 10*time.Second, config.Proxy)
  800. if err != nil {
  801. return err
  802. }
  803. request, err := http.NewRequestWithContext(ctx, http.MethodPost, base.String(), bytes.NewReader(body))
  804. if err != nil {
  805. return err
  806. }
  807. request.Header.Set("Content-Type", "application/json")
  808. request.Header.Set("User-Agent", "vocat-telegram-bot/1")
  809. response, err := client.Do(request)
  810. if err != nil {
  811. return err
  812. }
  813. defer response.Body.Close()
  814. responseBody, err := io.ReadAll(io.LimitReader(response.Body, 2<<20))
  815. if err != nil {
  816. return err
  817. }
  818. var envelope telegramAPIResponse
  819. if err := json.Unmarshal(responseBody, &envelope); err != nil {
  820. return fmt.Errorf("decode Telegram response: %w", err)
  821. }
  822. if response.StatusCode < 200 || response.StatusCode >= 300 || !envelope.OK {
  823. return fmt.Errorf("Telegram %s failed: HTTP %d %s", method, response.StatusCode, envelope.Description)
  824. }
  825. if result != nil && len(envelope.Result) != 0 {
  826. if err := json.Unmarshal(envelope.Result, result); err != nil {
  827. return fmt.Errorf("decode Telegram %s result: %w", method, err)
  828. }
  829. }
  830. return nil
  831. }
  832. func (bot *telegramBot) sendText(ctx context.Context, config telegramRuntimeConfig, chatID int64, text string, replyMarkup any) error {
  833. target := config.ChatID
  834. if chatID != 0 {
  835. target = strconv.FormatInt(chatID, 10)
  836. }
  837. payload := map[string]any{
  838. "chat_id": target,
  839. "text": truncateTelegramText(text, 3900),
  840. }
  841. if replyMarkup != nil {
  842. payload["reply_markup"] = replyMarkup
  843. }
  844. requestContext, cancel := context.WithTimeout(ctx, 10*time.Second)
  845. defer cancel()
  846. return bot.call(requestContext, config, "sendMessage", payload, nil)
  847. }
  848. func (bot *telegramBot) answerCallback(ctx context.Context, config telegramRuntimeConfig, callbackID, text string) error {
  849. payload := map[string]any{"callback_query_id": callbackID}
  850. if text != "" {
  851. payload["text"] = text
  852. }
  853. requestContext, cancel := context.WithTimeout(ctx, 8*time.Second)
  854. defer cancel()
  855. return bot.call(requestContext, config, "answerCallbackQuery", payload, nil)
  856. }
  857. func (bot *telegramBot) warn(message string, err error) {
  858. if err == nil || bot.server.logger == nil {
  859. return
  860. }
  861. now := time.Now()
  862. text := err.Error()
  863. bot.logMu.Lock()
  864. if text == bot.lastLogText && now.Sub(bot.lastLogTime) < time.Minute {
  865. bot.logMu.Unlock()
  866. return
  867. }
  868. bot.lastLogText, bot.lastLogTime = text, now
  869. bot.logMu.Unlock()
  870. bot.server.logger.Warn(message, "error", err)
  871. }
  872. func parseTelegramCommand(text string) (string, string) {
  873. text = strings.TrimSpace(text)
  874. if !strings.HasPrefix(text, "/") {
  875. return "", ""
  876. }
  877. commandToken, remainder, _ := strings.Cut(text, " ")
  878. commandToken = strings.TrimPrefix(commandToken, "/")
  879. if at := strings.IndexByte(commandToken, '@'); at >= 0 {
  880. commandToken = commandToken[:at]
  881. }
  882. return strings.ToLower(strings.TrimSpace(commandToken)), strings.TrimSpace(remainder)
  883. }
  884. func splitTelegramArguments(value string, count int) []string {
  885. fields := strings.Fields(value)
  886. if len(fields) == 0 || count <= 0 {
  887. return nil
  888. }
  889. if len(fields) <= count {
  890. return fields
  891. }
  892. result := append([]string(nil), fields[:count-1]...)
  893. return append(result, strings.Join(fields[count-1:], " "))
  894. }
  895. func validTelegramDialNumber(number string) bool {
  896. number = strings.TrimSpace(number)
  897. if strings.HasPrefix(number, "+") {
  898. number = number[1:]
  899. }
  900. if len(number) < 3 || len(number) > 20 {
  901. return false
  902. }
  903. for _, character := range number {
  904. if character < '0' || character > '9' {
  905. return false
  906. }
  907. }
  908. return true
  909. }
  910. func formatTelegramAT(response modem.Response) string {
  911. parts := make([]string, 0, 2)
  912. if text := strings.TrimSpace(response.Text()); text != "" {
  913. parts = append(parts, text)
  914. }
  915. if final := strings.TrimSpace(response.Final); final != "" {
  916. parts = append(parts, final)
  917. }
  918. if len(parts) == 0 {
  919. return "模块没有返回结果"
  920. }
  921. return strings.Join(parts, "\n")
  922. }
  923. func formatTelegramVoWiFiState(state vowifi.State) string {
  924. lines := []string{
  925. fmt.Sprintf("状态:%s", firstNonEmpty(string(state.Phase), "idle")),
  926. fmt.Sprintf("SIM=%t Access=%t Tunnel=%t IMS=%t SMS=%t", state.SIMReady, state.AccessReady, state.TunnelReady, state.IMSReady, state.SMSReady),
  927. }
  928. if state.LastReason != "" {
  929. lines = append(lines, "原因:"+state.LastReason)
  930. }
  931. if state.LastError != "" {
  932. lines = append(lines, "错误:"+state.LastError)
  933. }
  934. return strings.Join(lines, "\n")
  935. }
  936. func truncateTelegramText(value string, maximum int) string {
  937. runes := []rune(value)
  938. if maximum <= 0 || len(runes) <= maximum {
  939. return value
  940. }
  941. return string(runes[:maximum]) + "…"
  942. }
  943. func tailDigits(value string, count int) string {
  944. value = strings.TrimSpace(value)
  945. if count <= 0 || len(value) <= count {
  946. return value
  947. }
  948. return value[len(value)-count:]
  949. }
  950. func waitTelegram(ctx context.Context, duration time.Duration) bool {
  951. timer := time.NewTimer(duration)
  952. defer timer.Stop()
  953. select {
  954. case <-ctx.Done():
  955. return false
  956. case <-timer.C:
  957. return true
  958. }
  959. }