Telegram bot to control MQTT lights. Written in Go.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

lights-telegram.go 15KB

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