Telegram bot to control MQTT lights. Written in Go.
Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

lights-telegram.go 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. package main
  2. import (
  3. "fmt"
  4. "log"
  5. "os"
  6. "strings"
  7. "strconv"
  8. "errors"
  9. "io/ioutil"
  10. "gopkg.in/yaml.v3"
  11. "github.com/eclipse/paho.mqtt.golang"
  12. tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
  13. )
  14. var configFilename = "config.yaml"
  15. type Mqtt struct {
  16. Url string `yaml:"url"`
  17. User string `yaml:"username"`
  18. Pass string `yaml:"password"`
  19. }
  20. type Registration struct {
  21. Name string `yaml:"name"`
  22. Topic string `yaml:"topic"`
  23. Values []string `yaml:"values"`
  24. lastValue string
  25. }
  26. type Config struct {
  27. // Telegram Bot API key
  28. Key string `yaml:"api_key"`
  29. // Telegram UserID (int64) of admin account
  30. Admin int64 `yaml:"admin_id"`
  31. // MQTT credentials
  32. Mqtt Mqtt
  33. // Telegram UserIDs (int64) of allowed users
  34. // (does not need to be modified manually)
  35. Users []int64 `yaml:"authorized_users"`
  36. // Available MQTT topics
  37. // (does not need to be modified manually)
  38. Registration []Registration
  39. }
  40. // default values
  41. var config = Config {
  42. Key: "API_KEY_GOES_HERE",
  43. Admin: 0,
  44. Mqtt: Mqtt {
  45. Url: "wss://MQTT_HOST:MQTT_PORT",
  46. User: "MQTT_USERNAME",
  47. Pass: "MQTT_PASSWORD",
  48. },
  49. }
  50. var bot *tgbotapi.BotAPI = nil
  51. var mqttClient mqtt.Client = nil
  52. func readConfig() error {
  53. // read config file
  54. file, err := ioutil.ReadFile(configFilename)
  55. if err != nil {
  56. log.Printf("Conf file error: %v", err)
  57. return err
  58. }
  59. // parse yaml into struct
  60. err = yaml.Unmarshal(file, &config)
  61. if err != nil {
  62. log.Printf("Conf yaml error: %v", err)
  63. return err
  64. }
  65. return nil
  66. }
  67. func writeConfig() error {
  68. // parse struct into yaml
  69. data, err := yaml.Marshal(config)
  70. if err != nil {
  71. log.Printf("Conf yaml error: %v", err)
  72. return err
  73. }
  74. // write config file
  75. err = ioutil.WriteFile(configFilename, data, 0644)
  76. if err != nil {
  77. log.Printf("Conf file error: %v", err)
  78. return err
  79. }
  80. return nil
  81. }
  82. func isAdmin(id int64) bool {
  83. if id == config.Admin {
  84. return true
  85. }
  86. return false
  87. }
  88. func isAuthorizedUser(id int64) bool {
  89. if isAdmin(id) {
  90. return true
  91. }
  92. for user := range config.Users {
  93. if id == config.Users[user] {
  94. return true
  95. }
  96. }
  97. return false
  98. }
  99. func addAuthorizedUser(id int64) error {
  100. if isAdmin(id) {
  101. // admin is always authorized
  102. return nil
  103. }
  104. for user := range config.Users {
  105. if id == config.Users[user] {
  106. // already in users list
  107. return nil
  108. }
  109. }
  110. config.Users = append(config.Users, id)
  111. return writeConfig()
  112. }
  113. func sendReply(text string, chat int64, message int) {
  114. msg := tgbotapi.NewMessage(chat, text)
  115. msg.ReplyToMessageID = message
  116. msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true)
  117. _, err := bot.Send(msg)
  118. if err != nil {
  119. log.Printf("Bot error: %v", err)
  120. }
  121. }
  122. func sendMessage(text string, user int64) {
  123. // UserID == ChatID
  124. msg := tgbotapi.NewMessage(user, text)
  125. msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true)
  126. _, err := bot.Send(msg)
  127. if err != nil {
  128. log.Printf("Bot error: %v", err)
  129. }
  130. }
  131. func sendKeyboardReply(text string, name string, chat int64, message int) {
  132. var rows [][]tgbotapi.KeyboardButton
  133. for reg := range config.Registration {
  134. if name == config.Registration[reg].Name {
  135. for value := range config.Registration[reg].Values {
  136. button := tgbotapi.NewKeyboardButton("/" + name + " " + config.Registration[reg].Values[value])
  137. row := tgbotapi.NewKeyboardButtonRow(button)
  138. rows = append(rows, row)
  139. }
  140. }
  141. }
  142. keyboard := tgbotapi.NewOneTimeReplyKeyboard(rows...)
  143. msg := tgbotapi.NewMessage(chat, text)
  144. msg.ReplyToMessageID = message
  145. msg.ReplyMarkup = keyboard
  146. _, err := bot.Send(msg)
  147. if err != nil {
  148. log.Printf("Bot error: %v", err)
  149. }
  150. }
  151. func sendGenericKeyboardReply(text string, chat int64, message int) {
  152. var rows [][]tgbotapi.KeyboardButton
  153. //var rows []tgbotapi.KeyboardButton
  154. for reg := range config.Registration {
  155. button := tgbotapi.NewKeyboardButton("/" + config.Registration[reg].Name)
  156. row := tgbotapi.NewKeyboardButtonRow(button)
  157. //row := button
  158. rows = append(rows, row)
  159. }
  160. keyboard := tgbotapi.NewOneTimeReplyKeyboard(rows...)
  161. //keyboard := tgbotapi.NewOneTimeReplyKeyboard(rows)
  162. msg := tgbotapi.NewMessage(chat, text)
  163. msg.ReplyToMessageID = message
  164. msg.ReplyMarkup = keyboard
  165. _, err := bot.Send(msg)
  166. if err != nil {
  167. log.Printf("Bot error: %v", err)
  168. }
  169. }
  170. func notifyAdminAuthorization(id int64, name string) {
  171. if (config.Admin == 0) {
  172. // probably no admin account configured yet. don't ask them.
  173. return
  174. }
  175. log.Printf("Requesting admin authorization for new user %s.", name)
  176. text := fmt.Sprintf("New connection from %s. Send \"/auth %d\" to authorize.", name, id)
  177. sendMessage(text, config.Admin)
  178. }
  179. func sendMqttMessage(topic string, msg string) {
  180. log.Printf("MQTT Tx: %s @ %s", msg, topic)
  181. token := mqttClient.Publish(topic, 0, true, msg)
  182. token.Wait()
  183. }
  184. func register(name string, topic string, values string) error {
  185. for reg := range config.Registration {
  186. if name == config.Registration[reg].Name {
  187. return errors.New("already registered")
  188. }
  189. }
  190. v := strings.Split(values, ",")
  191. r := Registration {
  192. Name: name,
  193. Topic: topic,
  194. Values: v,
  195. }
  196. config.Registration = append(config.Registration, r)
  197. writeConfig()
  198. token := mqttClient.Subscribe(topic, 0, onMessageReceived)
  199. if token.Wait() && token.Error() != nil {
  200. log.Printf("MQTT sub error: %v", token.Error())
  201. }
  202. return nil
  203. }
  204. func remove(s []Registration, i int) []Registration {
  205. s[i] = s[len(s) - 1]
  206. return s[:len(s) - 1]
  207. }
  208. func unregister(name string) error {
  209. for reg := range config.Registration {
  210. if name == config.Registration[reg].Name {
  211. token := mqttClient.Unsubscribe(config.Registration[reg].Topic)
  212. if token.Wait() && token.Error() != nil {
  213. log.Println("MQTT unsub error: %v", token.Error())
  214. }
  215. config.Registration = remove(config.Registration, reg)
  216. writeConfig()
  217. return nil
  218. }
  219. }
  220. return errors.New("name not found")
  221. }
  222. func isRegisteredCommand(name string) bool {
  223. for reg := range config.Registration {
  224. if name == config.Registration[reg].Name {
  225. return true
  226. }
  227. }
  228. return false
  229. }
  230. func isValidValue(name string, val string) bool {
  231. for reg := range config.Registration {
  232. if name == config.Registration[reg].Name {
  233. for value := range config.Registration[reg].Values {
  234. if val == config.Registration[reg].Values[value] {
  235. return true
  236. }
  237. }
  238. }
  239. }
  240. return false
  241. }
  242. func topicForName(name string) string {
  243. for reg := range config.Registration {
  244. if name == config.Registration[reg].Name {
  245. return config.Registration[reg].Topic
  246. }
  247. }
  248. return "unknown"
  249. }
  250. func lastValueForCommand(name string) string {
  251. ret := ""
  252. for reg := range config.Registration {
  253. if name == config.Registration[reg].Name {
  254. if len(config.Registration[reg].lastValue) > 0 {
  255. ret = "Current state: \""
  256. ret += config.Registration[reg].lastValue
  257. ret += "\"\n"
  258. }
  259. break
  260. }
  261. }
  262. ret += "Select option below..."
  263. return ret
  264. }
  265. func onMessageReceived(client mqtt.Client, message mqtt.Message) {
  266. log.Printf("MQTT Rx: %s @ %s", message.Payload(), message.Topic())
  267. for reg := range config.Registration {
  268. if config.Registration[reg].Topic == message.Topic() {
  269. config.Registration[reg].lastValue = string(message.Payload()[:])
  270. }
  271. }
  272. }
  273. func main() {
  274. err := readConfig()
  275. if err != nil {
  276. log.Printf("Can't read config file \"%s\".", configFilename)
  277. log.Printf("Writing default values. Please modify.")
  278. writeConfig()
  279. os.Exit(1)
  280. }
  281. // MQTT debugging
  282. //mqtt.ERROR = log.New(os.Stdout, "[ERROR] ", 0)
  283. //mqtt.CRITICAL = log.New(os.Stdout, "[CRIT] ", 0)
  284. //mqtt.WARN = log.New(os.Stdout, "[WARN] ", 0)
  285. //mqtt.DEBUG = log.New(os.Stdout, "[DEBUG] ", 0)
  286. // Initialize MQTT
  287. opts := mqtt.NewClientOptions()
  288. opts.AddBroker(config.Mqtt.Url)
  289. opts.SetClientID("lights-telegram")
  290. opts.SetUsername(config.Mqtt.User)
  291. opts.SetPassword(config.Mqtt.Pass)
  292. mqttClient = mqtt.NewClient(opts)
  293. token := mqttClient.Connect();
  294. if token.Wait() && token.Error() != nil {
  295. log.Printf("MQTT error: %v", token.Error())
  296. os.Exit(1)
  297. }
  298. // Subscribe to registered topics
  299. for reg := range config.Registration {
  300. token := mqttClient.Subscribe(config.Registration[reg].Topic, 0, onMessageReceived)
  301. if token.Wait() && token.Error() != nil {
  302. log.Printf("MQTT sub error: %v", token.Error())
  303. }
  304. }
  305. // Initialize Telegram
  306. bot, err = tgbotapi.NewBotAPI(config.Key)
  307. if err != nil {
  308. log.Fatalf("Bot error: %v", err)
  309. }
  310. // Telegram debugging
  311. //bot.Debug = true
  312. // Start message receiving
  313. log.Printf("Authorized on account %s", bot.Self.UserName)
  314. u := tgbotapi.NewUpdate(0)
  315. u.Timeout = 60
  316. updates := bot.GetUpdatesChan(u)
  317. for update := range updates {
  318. if update.Message == nil {
  319. continue
  320. }
  321. log.Printf("[Rx \"%s\"] %s", update.Message.From.UserName, update.Message.Text)
  322. reply := ""
  323. showGenericKeyboard := false
  324. if isAuthorizedUser(update.Message.From.ID) {
  325. switch {
  326. case update.Message.Text == "/start":
  327. reply = "Welcome to the Lights control bot! Try /help for tips."
  328. showGenericKeyboard = true
  329. case update.Message.Text == "/help":
  330. if len(config.Registration) > 0 {
  331. reply += "You can use the following commands:\n"
  332. for reg := range config.Registration {
  333. reply += fmt.Sprintf(" - /%s", config.Registration[reg].Name)
  334. for val := range config.Registration[reg].Values {
  335. reply += fmt.Sprintf(" %s", config.Registration[reg].Values[val])
  336. }
  337. reply += "\n"
  338. }
  339. reply += "\n"
  340. }
  341. reply += "These commands are always available:\n"
  342. reply += " - /send TOPIC VALUE\n"
  343. reply += " - /help\n"
  344. reply += " - /start\n"
  345. if isAdmin(update.Message.From.ID) {
  346. reply += "\nYou are an administrator, so you can also use:\n"
  347. reply += " - /auth ID\n"
  348. reply += " - /register NAME TOPIC VAL1,VAL2,...\n"
  349. reply += " - /unregister NAME\n"
  350. reply += " - /commandlist"
  351. } else {
  352. reply += "\nAdministrators have further options not available to you."
  353. }
  354. showGenericKeyboard = true
  355. case strings.HasPrefix(update.Message.Text, "/auth "):
  356. if isAdmin(update.Message.From.ID) {
  357. id, err := strconv.ParseInt(update.Message.Text[6:], 10, 64)
  358. if err != nil {
  359. reply = fmt.Sprintf("Error parsing ID! %v", err)
  360. } else {
  361. err = addAuthorizedUser(id)
  362. if err != nil {
  363. reply = fmt.Sprintf("Error authorizing ID! %v", err)
  364. } else {
  365. reply = fmt.Sprintf("Ok, authorized %d.", id)
  366. // also notify user
  367. text := "You have now been authorized by the admin. Try /help for commands."
  368. sendMessage(text, id)
  369. }
  370. }
  371. } else {
  372. reply = "Sorry, only administrators can do that!"
  373. }
  374. case strings.HasPrefix(update.Message.Text, "/send "):
  375. s := update.Message.Text[6:]
  376. topic, msg, found := strings.Cut(s, " ")
  377. if found {
  378. reply = fmt.Sprintf("Setting \"%s\" to \"%s\"", topic, msg)
  379. sendMqttMessage(topic, msg)
  380. } else {
  381. reply = "Error parsing your message."
  382. }
  383. case strings.HasPrefix(update.Message.Text, "/register "):
  384. if isAdmin(update.Message.From.ID) {
  385. s := update.Message.Text[10:]
  386. name, rest, found := strings.Cut(s, " ")
  387. if found {
  388. topic, values, found := strings.Cut(rest, " ")
  389. if found {
  390. err = register(name, topic, values)
  391. if err != nil {
  392. reply = fmt.Sprintf("Error registering! %v", err)
  393. } else {
  394. reply = fmt.Sprintf("Ok, registered %s", name)
  395. }
  396. } else {
  397. reply = fmt.Sprintf("Error parsing topic!")
  398. }
  399. } else {
  400. reply = fmt.Sprintf("Error parsing name!")
  401. }
  402. } else {
  403. reply = "Sorry, only administrators can do that!"
  404. }
  405. case strings.HasPrefix(update.Message.Text, "/unregister "):
  406. if isAdmin(update.Message.From.ID) {
  407. name := update.Message.Text[12:]
  408. err = unregister(name)
  409. if err != nil {
  410. reply = fmt.Sprintf("Error unregistering! %v", err)
  411. } else {
  412. reply = fmt.Sprintf("Ok, unregistered %s", name)
  413. }
  414. } else {
  415. reply = "Sorry, only administrators can do that!"
  416. }
  417. case update.Message.Text == "/commandlist":
  418. if isAdmin(update.Message.From.ID) {
  419. for reg := range config.Registration {
  420. reply += fmt.Sprintf("%s - Set '%s' state\n", config.Registration[reg].Name, config.Registration[reg].Topic)
  421. }
  422. reply += "help - Show help text and keyboard"
  423. } else {
  424. reply = "Sorry, only administrators can do that!"
  425. }
  426. default:
  427. reply = "Sorry, I did not understand. Try /help instead."
  428. name, val, found := strings.Cut(update.Message.Text, " ")
  429. name = name[1:] // remove '/'
  430. if found {
  431. if isRegisteredCommand(name) {
  432. if isValidValue(name, val) {
  433. sendMqttMessage(topicForName(name), val)
  434. reply = fmt.Sprintf("Ok, setting %s to %s", name, val)
  435. } else {
  436. reply = "Sorry, this is not a valid value! Try /help instead."
  437. }
  438. } else {
  439. reply = "Sorry, this command is not registered. Try /help instead."
  440. }
  441. } else if isRegisteredCommand(update.Message.Text[1:]) {
  442. reply = ""
  443. text := lastValueForCommand(update.Message.Text[1:])
  444. sendKeyboardReply(text, update.Message.Text[1:], update.Message.Chat.ID, update.Message.MessageID)
  445. }
  446. }
  447. } else {
  448. // only request admin-auth when /start has been sent!
  449. // this avoids most bot spam.
  450. if update.Message.Text == "/start" {
  451. log.Printf("Message from unauthorized user. %s %d", update.Message.From.UserName, update.Message.From.ID)
  452. notifyAdminAuthorization(update.Message.From.ID, update.Message.From.UserName)
  453. reply = "Sorry, you are not authorized. Administrator confirmation required."
  454. }
  455. }
  456. // send a reply
  457. if reply != "" {
  458. log.Printf("[Tx \"%s\"] %s", update.Message.From.UserName, reply)
  459. if showGenericKeyboard {
  460. sendGenericKeyboardReply(reply, update.Message.Chat.ID, update.Message.MessageID)
  461. } else {
  462. sendReply(reply, update.Message.Chat.ID, update.Message.MessageID)
  463. }
  464. }
  465. }
  466. }