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 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. }