소스 검색

initial commit

Thomas Buck 2 년 전
커밋
2e1355a56c
5개의 변경된 파일592개의 추가작업 그리고 0개의 파일을 삭제
  1. 35
    0
      README.md
  2. 40
    0
      config.yaml
  3. 15
    0
      go.mod
  4. 18
    0
      go.sum
  5. 484
    0
      lights-telegram.go

+ 35
- 0
README.md 파일 보기

@@ -0,0 +1,35 @@
1
+# Lights-Telegram
2
+
3
+Simple MQTT bridge in the form of a Telegram Bot.
4
+And also my first attempt at doing something with Go.
5
+
6
+## Getting Started
7
+
8
+First register a new Bot with the Telegram, as [described in the docs](https://core.telegram.org/bots#6-botfather), to get your API key.
9
+
10
+Then simply run lights-telegram once manually.
11
+This will create a `config.yaml` file in the same directory.
12
+Your API key goes in there, as well as the MQTT credentials.
13
+
14
+With the config prepared, run lights-telegram again.
15
+Now send a message to the bot.
16
+You will see the UserID of your Telegram account in the log output.
17
+Quit lights-telegram again by pressing Ctrl+C, then open `config.yaml` again and put your User ID into the `admin_id` field.
18
+Now this admin account can add further user authorizations using chat messages to the bot.
19
+
20
+As an admin use `/register` to add new MQTT topics and their possible values to the menu.
21
+Finally run `/commandlist` on the bot to get a nicely formatted command list which you can then give to the `/setcommands` command of BotFather.
22
+
23
+## Dependencies
24
+
25
+ * [tgbotapi](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5)
26
+ * [mqtt](https://pkg.go.dev/github.com/eclipse/paho.mqtt.golang)
27
+
28
+## License
29
+
30
+    ----------------------------------------------------------------------------
31
+    "THE BEER-WARE LICENSE" (Revision 42):
32
+    <xythobuz@xythobuz.de> wrote this file.  As long as you retain this notice
33
+    you can do whatever you want with this stuff. If we meet some day, and you
34
+    think this stuff is worth it, you can buy me a beer in return.   Thomas Buck
35
+    ----------------------------------------------------------------------------

+ 40
- 0
config.yaml 파일 보기

@@ -0,0 +1,40 @@
1
+api_key: API_KEY_HERE
2
+admin_id: 42
3
+mqtt:
4
+    url: wss://HOST:PORT
5
+    username: USERNAME
6
+    password: PASSWORD
7
+authorized_users: []
8
+registration:
9
+    - name: kitchen
10
+      topic: livingroom/light_kitchen
11
+      values:
12
+        - "on"
13
+        - "off"
14
+    - name: bathroom
15
+      topic: bathroom/force_light
16
+      values:
17
+        - none
18
+        - big
19
+        - small
20
+        - "off"
21
+    - name: pc
22
+      topic: livingroom/light_pc
23
+      values:
24
+        - "on"
25
+        - "off"
26
+    - name: workbench
27
+      topic: livingroom/light_bench
28
+      values:
29
+        - "on"
30
+        - "off"
31
+    - name: box
32
+      topic: livingroom/light_box
33
+      values:
34
+        - "on"
35
+        - "off"
36
+    - name: amp
37
+      topic: livingroom/light_amp
38
+      values:
39
+        - "on"
40
+        - "off"

+ 15
- 0
go.mod 파일 보기

@@ -0,0 +1,15 @@
1
+module lights-telegram
2
+
3
+go 1.19
4
+
5
+require (
6
+	github.com/eclipse/paho.mqtt.golang v1.4.1
7
+	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
8
+	gopkg.in/yaml.v3 v3.0.1
9
+)
10
+
11
+require (
12
+	github.com/gorilla/websocket v1.4.2 // indirect
13
+	golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 // indirect
14
+	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
15
+)

+ 18
- 0
go.sum 파일 보기

@@ -0,0 +1,18 @@
1
+github.com/eclipse/paho.mqtt.golang v1.4.1 h1:tUSpviiL5G3P9SZZJPC4ZULZJsxQKXxfENpMvdbAXAI=
2
+github.com/eclipse/paho.mqtt.golang v1.4.1/go.mod h1:JGt0RsEwEX+Xa/agj90YJ9d9DH2b7upDZMK9HRbFvCA=
3
+github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
4
+github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
5
+github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
6
+github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
7
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
8
+golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0 h1:Jcxah/M+oLZ/R4/z5RzfPzGbPXnVDPkEDtf2JnuxN+U=
9
+golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
10
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ=
11
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
12
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
13
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
14
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
15
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
16
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
17
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
18
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 484
- 0
lights-telegram.go 파일 보기

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

Loading…
취소
저장