-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathtbcomctl.go
282 lines (236 loc) · 7.62 KB
/
tbcomctl.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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
// Package tbcomctl provides common controls for telegram bots.
package tbcomctl
import (
"context"
"crypto/sha1"
"fmt"
"strconv"
"golang.org/x/text/language"
tb "gopkg.in/telebot.v3"
"github.com/rusq/tbcomctl/v4/internal/registry"
)
const (
// FallbackLang is the default fallback language.
FallbackLang = "en-US"
)
type overwriter interface {
setOverwrite(b bool)
}
type commonCtl struct {
name string // name of the control, must be unique if used within chained controls.
prev Controller // if nil - this is the first controller in the chain
next Controller // if nil - this is the last controller in the chain.
form *Form // if not nil, controller is part of the form.
hError ErrorHandler // custom error handler.
privateOnly bool // should handle only private messages
overwrite bool // overwrite the previous message sent by control.
fallbackLang string // fallback language for i18n
sendOpts *tb.SendOptions // default send options.
reg *registry.Memory
}
// PrivateOnly is the middleware that restricts the handler to only private
// messages.
func PrivateOnly(fn tb.HandlerFunc) tb.HandlerFunc {
return PrivateOnlyMsg("", fn)
}
// PrivateOnlyMsg returns the handler that will reject non-private messages (eg.
// sent in groups) with i18n formatted message.
func PrivateOnlyMsg(msg string, fn tb.HandlerFunc) tb.HandlerFunc {
return func(c tb.Context) error {
if !c.Message().Private() {
if msg != "" {
pr := Printer(c.Sender().LanguageCode)
return c.Send(pr.Sprintf(msg))
}
return nil
}
return fn(c)
}
}
type controllerKey int // controller key type for context
var ctrlKey controllerKey // controller key for context.
// WithController adds the controller to the context.
func WithController(ctx context.Context, ctrl Controller) context.Context {
return context.WithValue(ctx, ctrlKey, ctrl)
}
// ControllerFromCtx returns the controller from the context.
func ControllerFromCtx(ctx context.Context) (Controller, bool) {
ctrl, ok := ctx.Value(ctrlKey).(Controller)
return ctrl, ok
}
// StoredMessage represents the stored message in the database.
type StoredMessage struct {
MessageID string
ChatID int64
}
func (m StoredMessage) MessageSig() (string, int64) {
return m.MessageID, m.ChatID
}
var hasher = sha1.New
// hash returns the hash of the s, using the hasher function.
func hash(s string) string {
h := hasher()
h.Write([]byte(s))
return fmt.Sprintf("%x", h.Sum(nil))
}
// option is the function signature for options that are common to all the
// controls. Concrete control implementations should use these options, if they
// must implement this functionality.
type option func(ctl *commonCtl)
// optPrivateOnly sets the control to be operatable only in the private mode.
func optPrivateOnly(b bool) option {
return func(ctl *commonCtl) {
ctl.privateOnly = b
}
}
// optErrFunc sets the error handler.
func optErrFunc(h ErrorHandler) option {
return func(ctl *commonCtl) {
ctl.hError = h
}
}
// optFallbackLang sets the default fallback language for the control.
func optFallbackLang(lang string) option {
return func(ctl *commonCtl) {
_ = language.MustParse(lang) // will panic if wrong.
ctl.fallbackLang = lang
}
}
// optDefaultSendOpts allows to set the default send options. If this option is
// not in the option list, the built-in defaults are used.
func optDefaultSendOpts(opts *tb.SendOptions) option {
return func(ctl *commonCtl) {
ctl.sendOpts = opts
}
}
// newCommonCtl creates a new commonCtl instance. It gives most of the
// functions that satisfy Controller interface for free.
func newCommonCtl(name string) commonCtl {
return commonCtl{
name: name,
reg: registry.NewMemRegistry(),
sendOpts: &tb.SendOptions{ParseMode: tb.ModeHTML},
}
}
// multibuttonMarkup returns a markup containing a bunch of buttons. If
// showCounter is true, will show a counter beside each of the labels. each
// telegram button will have a button index pressed by the user in the
// callback.Data. Prefix is the prefix that will be prepended to the unique
// before hash is called to form the Control-specific unique fields.
func (cc *commonCtl) multibuttonMarkup(b *tb.Bot, btns []Button, showCounter bool, prefix string, cbFn func(tb.Context) error) *tb.ReplyMarkup {
const (
sep = ": "
)
if cbFn == nil {
panic("internal error: callback function is empty")
}
markup := new(tb.ReplyMarkup)
var buttons []tb.Btn
for i, ri := range btns {
bn := markup.Data(ri.label(showCounter, sep), hash(prefix+ri.Name), strconv.Itoa(i))
buttons = append(buttons, bn)
b.Handle(&bn, cbFn)
}
markup.Inline(OrganizeButtons(buttons, defNumButtons)...)
return markup
}
// SetNext sets next controller in the chain.
func (cc *commonCtl) SetNext(ctrl Controller) {
if ctrl != nil {
cc.next = ctrl
}
}
// SetPrev sets the previous controller in the chain.
func (cc *commonCtl) SetPrev(ctrl Controller) {
if ctrl != nil {
cc.prev = ctrl
}
}
// NewControllerChain returns the controller chain.
//
// Deprecated: use NewForm instead. NewControllerChain will be removed in the next versions.
func NewControllerChain(first Controller, cc ...Controller) tb.HandlerFunc {
var chain Controller
for i := len(cc) - 1; i >= 0; i-- {
cc[i].SetNext(chain)
chain = cc[i]
}
first.SetNext(chain)
return first.Handler
}
// Name returns the controller name.
func (cc *commonCtl) Name() string {
return cc.name
}
// SetForm links the controller to the form.
func (cc *commonCtl) SetForm(fm *Form) {
cc.form = fm
}
// Form returns the form.
func (cc *commonCtl) Form() *Form {
return cc.form
}
// setOverwite sets overwrite flag to b.
func (cc *commonCtl) setOverwrite(b bool) {
cc.overwrite = b
}
// sendOrEdit sends the message or edits the previous one if the overwrite flag is true. It returns the outbound
// message and an error.
func (cc *commonCtl) sendOrEdit(c tb.Context, txt string, sendOpts ...interface{}) (*tb.Message, error) {
var outbound *tb.Message
var err error
msgID, ok := cc.getPreviousMsgID(c)
if cc.overwrite && ok {
prevMsg := tb.Message{ID: msgID, Chat: c.Chat()}
outbound, err = c.Bot().Edit(&prevMsg,
txt,
sendOpts...,
)
} else {
outbound, err = c.Bot().Send(c.Chat(), txt, sendOpts...)
}
return outbound, err
}
// getPreviousMsgID returns the ID of the previous outbound message.
func (cc *commonCtl) getPreviousMsgID(ct tb.Context) (int, bool) {
if cc.isBackPressed(ct) {
cc.resetBackPressed(ct)
if cc.next == nil {
// internal error
return 0, false
}
return cc.next.OutgoingID(ct.Sender().Recipient())
}
// back not pressed
if cc.prev == nil {
return 0, false
}
return cc.prev.OutgoingID(ct.Sender().Recipient())
}
// isBackPressed returns true if the "back" button was pressed.
func (cc *commonCtl) isBackPressed(ct tb.Context) bool {
backPressed, ok := ct.Get(BackPressed.Error()).(bool)
return ok && backPressed
}
func (cc *commonCtl) setBackPressed(ct tb.Context) {
ct.Set(BackPressed.Error(), true)
}
func (cc *commonCtl) resetBackPressed(ct tb.Context) {
ct.Set(BackPressed.Error(), false) // reset the context value
}
func unexpectedErrorText(c tb.Context, fallbackLang ...string) string {
pr := PrinterContext(c, fallbackLang...)
return pr.Sprintf(MsgUnexpected)
}
// OutgoingID returns the controller's outgoing message ID for the user.
func (cc *commonCtl) OutgoingID(recipient string) (int, bool) {
return cc.reg.OutgoingID(recipient)
}
// Value returns the Controller value for the recipient.
func (cc *commonCtl) Value(recipient string) (string, bool) {
return cc.reg.Value(recipient)
}
// SetValue sets the Controller value.
func (cc *commonCtl) SetValue(recipient string, value string) {
cc.reg.SetValue(recipient, value)
}