/
telegram.go
143 lines (129 loc) · 3.67 KB
/
telegram.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package telegram
import (
"encoding/json"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus"
"net/http"
)
// Client is an interface for interacting with the Telegram API.
type Client interface {
Webhook(domain string, f OnMessage) (*Webhook, error)
Poll(f OnMessage) error
}
// Message represents a message received from Telegram.
type Message struct {
Text string
replyFunc func(reply *Reply) error
}
// Reply sends a reply to the message that triggered the given message.
func (m *Message) Reply(reply *Reply) error {
return m.replyFunc(reply)
}
type Reply struct {
Text string
Styled bool
}
// OnMessage is a function that is called for each message received.
type OnMessage func(msg *Message) error
type clientImpl struct {
bot *tgbotapi.BotAPI
}
func New(token string) (Client, error) {
if token == "" {
return nil, errors.New("failed to read empty token")
}
bot, err := tgbotapi.NewBotAPI(token)
if err != nil {
return nil, errors.Wrap(err, "failed to construct bot api")
}
cl := &clientImpl{bot: bot}
return cl, nil
}
type Webhook struct {
Handler http.Handler
}
// Webhook registers a webhook for the given link and returns a Webhook struct containing the webhook path and handler.
func (c *clientImpl) Webhook(link string, f OnMessage) (*Webhook, error) {
if link == "" {
return nil, errors.New("failed to read empty domain")
}
wh, err := tgbotapi.NewWebhook(link)
if err != nil {
return nil, errors.Wrap(err, "failed to initialize webhook")
}
_, err = c.bot.Request(wh)
if err != nil {
return nil, errors.Wrap(err, "failed to request webhook creation")
}
h := c.handler(f)
return &Webhook{
Handler: http.HandlerFunc(h),
}, nil
}
func (c *clientImpl) handler(f OnMessage) func(w http.ResponseWriter, r *http.Request) {
writeError := func(w http.ResponseWriter, error string, status int) {
errMsg, _ := json.Marshal(map[string]string{"error": error})
w.WriteHeader(status)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(errMsg)
}
return func(w http.ResponseWriter, r *http.Request) {
update, err := c.bot.HandleUpdate(r)
if err != nil {
log.Errorf("failed to handle update: %v", err)
writeError(w, err.Error(), http.StatusBadRequest)
return
}
msg := c.message(update)
err = f(msg)
if err != nil {
log.Errorf("failed to process message: %v", err)
err = msg.Reply(&Reply{
Text: "Try again",
})
if err != nil {
log.Errorf("failed to reply to message: %v", err)
}
}
w.WriteHeader(http.StatusOK)
}
}
func (c *clientImpl) message(update *tgbotapi.Update) *Message {
return &Message{
Text: update.Message.Text,
replyFunc: func(reply *Reply) error {
m := tgbotapi.NewMessage(update.Message.Chat.ID, reply.Text)
m.ReplyToMessageID = update.Message.MessageID
if reply.Styled {
m.ParseMode = tgbotapi.ModeMarkdown
}
_, err := c.bot.Send(m)
return err
},
}
}
// Poll starts polling for messages and calls the given function f for each message received.
// It closes the webhook on the bot before starting to poll.
func (c *clientImpl) Poll(f OnMessage) error {
// Close webhook
_, err := c.bot.Send(tgbotapi.DeleteWebhookConfig{DropPendingUpdates: false})
if err != nil {
return errors.Wrap(err, "failed to remove webhook")
}
ch := c.bot.GetUpdatesChan(tgbotapi.UpdateConfig{})
for update := range ch {
msg := c.message(&update)
err := f(msg)
if err != nil {
log.Errorf("failed to process message: %v", err)
err = msg.Reply(&Reply{
Text: "Try again",
})
if err != nil {
log.Errorf("failed to reply to message: %v", err)
}
}
}
return errors.New("failed to receive updates")
}