Making the widgets more modulable. Needs to be finished.

This commit is contained in:
Andrey Parhomenko 2023-09-16 14:34:17 +03:00
parent 5e1faf0c44
commit 57f85fdacc
15 changed files with 464 additions and 270 deletions

View file

@ -4,29 +4,33 @@ tmp_dir = "tmp"
[build]
args_bin = []
bin = "./exe/test.exe"
cmd = "go build -o ./exe/ ./cmd/..."
bin = "exe/test.exe"
cmd = "go build -o ./exe/ ./cmd/test"
delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_dir = ["app/volume", "assets", "tmp", "vendor", "testdata"]
exclude_file = []
exclude_regex = ["_test.go"]
exclude_regex = ["_test.go", ".exe"]
exclude_unchanged = false
follow_symlink = false
full_bin = ""
include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"]
include_file = []
kill_delay = "0s"
log = "build-errors.log"
poll = false
poll = true
poll_interval = 0
rerun = false
rerun_delay = 500
send_interrupt = false
send_interrupt = true
stop_on_error = true
[color]
app = ""
app = "red"
build = "yellow"
main = "magenta"
runner = "green"

View file

@ -26,18 +26,21 @@ func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget {
return ret
}
func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) error {
func (w *MutateMessageWidget) Serve(c *tg.Context, updates *tg.UpdateChan) {
for _, arg := range c.Args {
c.Sendf("%v", arg)
}
for u := range updates {
if u.Message == nil {
continue
}
for u := range updates.Chan() {
text := u.Message.Text
c.Sendf("%s", w.Mutate(text))
}
return nil
}
func (w *MutateMessageWidget) Filter(u *tg.Update, _ tg.MessageMap) bool {
if u.Message == nil {
return true
}
return false
}
func ExtractSessionData(c *tg.Context) *SessionData {
@ -116,14 +119,14 @@ var beh = tg.NewBehaviour().
c.Session.Data = &SessionData{}
}).WithScreens(
tg.NewScreen("start", tg.NewPage(
"The bot started!",
"",
).WithInline(
tg.NewInline().Row(
tg.NewButton("GoT Github page").
WithUrl("https://github.com/mojosa-software/got"),
),
).Widget(""),
).WithReply(
navKeyboard,
navKeyboard.Widget("The bot started!"),
),
),
tg.NewScreen("start/inc-dec", tg.NewPage(
@ -132,7 +135,7 @@ var beh = tg.NewBehaviour().
"by saving the counter for each of users "+
"separately. ",
).WithReply(
incDecKeyboard,
incDecKeyboard.Widget("Press the buttons to increment and decrement"),
).ActionFunc(func(c *tg.Context) {
// The function will be calleb before serving page.
d := ExtractSessionData(c)
@ -143,7 +146,7 @@ var beh = tg.NewBehaviour().
tg.NewScreen("start/upper-case", tg.NewPage(
"Type text and the bot will send you the upper case version to you",
).WithReply(
navToStartKeyboard,
navToStartKeyboard.Widget(""),
).WithSub(
NewMutateMessageWidget(strings.ToUpper),
),
@ -152,7 +155,7 @@ var beh = tg.NewBehaviour().
tg.NewScreen("start/lower-case", tg.NewPage(
"Type text and the bot will send you the lower case version",
).WithReply(
navToStartKeyboard,
navToStartKeyboard.Widget(""),
).WithSub(
NewMutateMessageWidget(strings.ToLower),
),
@ -161,7 +164,7 @@ var beh = tg.NewBehaviour().
tg.NewScreen("start/send-location", tg.NewPage(
"Send your location and I will tell where you are!",
).WithReply(
sendLocationKeyboard,
sendLocationKeyboard.Widget(""),
).WithInline(
tg.NewInline().Row(
tg.NewButton(
@ -172,7 +175,7 @@ var beh = tg.NewBehaviour().
d := ExtractSessionData(c)
c.Sendf("Counter = %d", d.Counter)
}),
),
).Widget("Press the button to display your counter"),
),
),
).WithCommands(
@ -189,9 +192,9 @@ var beh = tg.NewBehaviour().
}),
tg.NewCommand("read").
Desc("reads a string and sends it back").
WidgetFunc(func(c *tg.Context, updates chan *tg.Update) error {
WidgetFunc(func(c *tg.Context, updates *tg.UpdateChan) {
c.Sendf("Type text and I will send it back to you")
for u := range updates {
for u := range updates.Chan() {
if u.Message == nil {
continue
}
@ -199,7 +202,6 @@ var beh = tg.NewBehaviour().
break
}
c.Sendf("Done")
return nil
}),
tg.NewCommand("image").
Desc("sends a sample image").

View file

@ -50,9 +50,9 @@ func (bot *Bot) Debug(debug bool) *Bot {
func (bot *Bot) Send(
sid SessionId, v Sendable,
) (*Message, error) {
config, err := v.SendConfig(sid, bot)
if err != nil {
return nil, err
config := v.SendConfig(sid, bot)
if config.Error != nil {
return nil, config.Error
}
msg, err := bot.Api.Send(config.ToApi())
@ -64,18 +64,22 @@ func (bot *Bot) Send(
func (bot *Bot) Render(
sid SessionId, r Renderable,
) ([]*Message, error) {
configs, err := r.Render(sid, bot)
if err != nil {
return []*Message{}, err
) (MessageMap, error) {
configs := r.Render(sid, bot)
if configs == nil {
return nil, MapCollisionErr
}
messages := []*Message{}
messages := make(MessageMap)
for _, config := range configs {
_, collision := messages[config.Name]
if collision {
return messages, MapCollisionErr
}
msg, err := bot.Api.Send(config.ToApi())
if err != nil {
return messages, err
}
messages = append(messages, &msg)
messages[config.Name] = &msg
}
return messages, nil
}
@ -204,7 +208,7 @@ func (bot *Bot) Run() error {
// The function handles updates supposed for the private
// chat with the bot.
func (bot *Bot) handlePrivate(updates chan *Update) {
chans := make(map[SessionId]chan *Update)
chans := make(map[SessionId] *UpdateChan )
var sid SessionId
for u := range updates {
sid = SessionId(u.FromChat().ID)
@ -224,9 +228,12 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
Bot: bot,
Session: session,
}
chn := make(chan *Update)
chn := NewUpdateChan()
chans[sid] = chn
go ctx.handleUpdateChan(chn)
go (&Context{
context: ctx,
Update: u,
}).Serve(chn)
}
} else if u.Message != nil {
// Create session on any message
@ -237,16 +244,19 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
Bot: bot,
Session: lsession,
}
chn := make(chan *Update)
chn := NewUpdateChan()
chans[sid] = chn
go ctx.handleUpdateChan(chn)
go (&Context{
context: ctx,
Update: u,
}).Serve(chn)
}
chn, ok := chans[sid]
// The bot MUST get the "start" command.
// It will do nothing otherwise.
if ok {
chn <- u
chn.Send(u)
}
}
}

View file

@ -142,7 +142,21 @@ func (w *CommandWidget) WithUsageFunc(fn ActionFunc) *CommandWidget {
return w.WithUsage(fn)
}
func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
func (widget *Command) Filter(
u *Update,
msgs ...*Message,
) bool {
/*if u.Message == nil || !u.Message.IsCommand() {
return false
}*/
return false
}
func (widget *CommandWidget) Serve(
c *Context,
updates *UpdateChan,
) {
commanders := make(map[CommandName] BotCommander)
for k, v := range widget.Commands {
commanders[k] = v
@ -153,7 +167,7 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
)
var cmdUpdates chan *Update
for u := range updates {
for u := range updates.Chan() {
if c.ScreenId() == "" && u.Message != nil {
// Skipping and executing the preinit action
// while we have the empty screen.
@ -178,13 +192,12 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
if cmdUpdates != nil {
close(cmdUpdates)
}
cmdUpdates = make(chan *Update)
cmdUpdates := NewUpdateChan()
go func() {
cmd.Widget.Serve(
&Context{context: c.context, Update: u},
cmdUpdates,
)
close(cmdUpdates)
cmdUpdates = nil
}()
}
@ -199,5 +212,4 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
c.Skip(u)
}
}
return nil
}

View file

@ -16,6 +16,7 @@ var (
NotAvailableErr = errors.New("the context is not available")
EmptyKeyboardTextErr = errors.New("got empty text for a keyboard")
ActionNotDefinedErr = errors.New("action was not defined")
MapCollisionErr = errors.New("map collision occured")
)
func (wut WrongUpdateType) Error() string {

View file

@ -74,7 +74,7 @@ func (f *File) SendData() string {
}
func (f *File) SendConfig(
sid SessionId, bot *Bot,
) (*SendConfig, error) {
) (*SendConfig) {
var config SendConfig
cid := sid.ToApi()
@ -85,9 +85,9 @@ func (f *File) SendConfig(
config.Image = &photo
default:
return nil, UnknownFileTypeErr
panic(UnknownFileTypeErr)
}
return &config, nil
return &config
}

View file

@ -83,13 +83,9 @@ func (c *groupContext) Sendf(
format string,
v ...any,
) (*Message, error) {
msg, err := c.Send(NewMessage(
c.Session.Id, fmt.Sprintf(format, v...),
return c.Send(NewMessage(
fmt.Sprintf(format, v...),
))
if err != nil {
return nil, err
}
return msg, err
}
// Sends into the chat specified values converted to strings.

View file

@ -10,6 +10,7 @@ type Keyboard struct {
// defined action for the button.
Action *action
Rows []ButtonRow
buttonMap ButtonMap
}
// The type represents reply keyboards.
@ -59,6 +60,14 @@ func (kbd *InlineKeyboard) ActionFunc(fn ActionFunc) *InlineKeyboard {
return kbd.WithAction(fn)
}
// Transform the keyboard to widget with the specified text.
func (kbd *InlineKeyboard) Widget(text string) *InlineKeyboardWidget {
ret := &InlineKeyboardWidget{}
ret.InlineKeyboard = kbd
ret.Text = text
return ret
}
// Adds a new button row to the current keyboard.
func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
// For empty row. We do not need that.
@ -80,6 +89,13 @@ func (kbd *ReplyKeyboard) ActionFunc(fn ActionFunc) *ReplyKeyboard {
return kbd.WithAction(fn)
}
func (kbd *ReplyKeyboard) Widget(text string) *ReplyKeyboardWidget {
ret := &ReplyKeyboardWidget{}
ret.ReplyKeyboard = kbd
ret.Text = text
return ret
}
// Convert the Keyboard to the Telegram API type of reply keyboard.
func (kbd *ReplyKeyboard) ToApi() any {
// Shades everything.
@ -133,12 +149,16 @@ func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard {
// Returns the map of buttons. Used to define the Action.
func (kbd Keyboard) ButtonMap() ButtonMap {
if kbd.buttonMap != nil {
return kbd.buttonMap
}
ret := make(ButtonMap)
for _, vi := range kbd.Rows {
for _, vj := range vi {
ret[vj.Key()] = vj
}
}
kbd.buttonMap = ret
return ret
}

View file

@ -4,48 +4,23 @@ import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// Simple text message type.
type MessageConfig struct {
To SessionId
ReplyTo MessageId
Text string
Inline *InlineKeyboard
Reply *ReplyKeyboard
}
func NewMessage(to SessionId, text string) *MessageConfig {
// Return new message with the specified text.
func NewMessage(text string) *MessageConfig {
ret := &MessageConfig{}
ret.To = to
ret.Text = text
return ret
}
func (config *MessageConfig) WithInline(
inline *InlineKeyboard,
) *MessageConfig {
config.Inline = inline
return config
}
func (config *MessageConfig) WithReply(
reply *ReplyKeyboard,
) *MessageConfig {
config.Reply = reply
return config
}
func (config *MessageConfig) SendConfig(
sid SessionId, bot *Bot,
) (*SendConfig, error) {
) (*SendConfig) {
var ret SendConfig
msg := tgbotapi.NewMessage(config.To.ToApi(), config.Text)
if config.Inline != nil {
msg.ReplyMarkup = config.Inline.ToApi()
}
// Reply shades the inline.
if config.Reply != nil {
msg.ReplyMarkup = config.Reply.ToApi()
}
msg := tgbotapi.NewMessage(sid.ToApi(), config.Text)
ret.Message = &msg
return &ret, nil
return &ret
}

View file

@ -1,7 +1,7 @@
package tg
import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
//tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// The basic widget to provide keyboard functionality
@ -9,9 +9,9 @@ import (
type Page struct {
Text string
SubWidget Widget
Inline *InlineKeyboard
Reply *ReplyKeyboard
Inline *InlineKeyboardWidget
Action Action
Reply *ReplyKeyboardWidget
}
// Return new page with the specified text.
@ -22,13 +22,13 @@ func NewPage(text string) *Page {
}
// Set the inline keyboard.
func (p *Page) WithInline(inline *InlineKeyboard) *Page {
func (p *Page) WithInline(inline *InlineKeyboardWidget) *Page {
p.Inline = inline
return p
}
// Set the reply keyboard.
func (p *Page) WithReply(reply *ReplyKeyboard) *Page {
func (p *Page) WithReply(reply *ReplyKeyboardWidget) *Page {
p.Reply = reply
return p
}
@ -51,150 +51,76 @@ func (p *Page) WithSub(sub Widget) *Page {
return p
}
func (p *Page) Serve(
c *Context, updates chan *Update,
) error {
msgs, err := c.Render(p)
if err != nil {
return err
}
// The inline message is always returned
// and the reply one is useless in our case.
inlineMsg := msgs[0]
if p.Action != nil {
c.run(p.Action, c.Update)
}
var subUpdates chan *Update
if p.SubWidget != nil {
subUpdates = make(chan *Update)
go p.SubWidget.Serve(c, subUpdates)
defer close(subUpdates)
}
for u := range updates {
var act Action
if u.Message != nil {
text := u.Message.Text
kbd := p.Reply
if kbd == nil {
if subUpdates != nil {
subUpdates <- u
}
continue
}
btns := kbd.ButtonMap()
btn, ok := btns[text]
if !ok {
if u.Message.Location != nil {
for _, b := range btns {
if b.SendLocation {
btn = b
ok = true
}
}
} else if subUpdates != nil {
subUpdates <- u
}
}
if btn != nil {
act = btn.Action
} else if kbd.Action != nil {
act = kbd.Action
}
} else if u.CallbackQuery != nil {
if u.CallbackQuery.Message.MessageID != inlineMsg.MessageID {
if subUpdates != nil {
subUpdates <- u
}
continue
}
cb := tgbotapi.NewCallback(
u.CallbackQuery.ID,
u.CallbackQuery.Data,
)
data := u.CallbackQuery.Data
_, err := c.Bot.Api.Request(cb)
if err != nil {
return err
}
kbd := p.Inline
if kbd == nil {
if subUpdates != nil {
subUpdates <- u
}
continue
}
btns := kbd.ButtonMap()
btn, ok := btns[data]
if !ok {
if subUpdates != nil {
subUpdates <- u
}
continue
}
if btn != nil {
act = btn.Action
} else if kbd.Action != nil {
act = kbd.Action
}
}
c.Run(act, u)
}
return nil
}
func (s *Page) Render(
func (p *Page) Render(
sid SessionId, bot *Bot,
) ([]*SendConfig, error) {
cid := sid.ToApi()
reply := s.Reply
inline := s.Inline
) ([]*SendConfig) {
reply := p.Reply
inline := p.Inline
ret := []*SendConfig{}
var txt string
// Screen text and inline keyboard.
if s.Text != "" {
txt = s.Text
} else if inline != nil {
// Default to send the keyboard.
txt = ">"
if p.Text != "" {
cfg := NewMessage(p.Text).SendConfig(sid, bot).
WithName("page/text")
ret = append(ret, cfg)
}
if txt != "" {
msgConfig := tgbotapi.NewMessage(cid, txt)
if inline != nil {
msgConfig.ReplyMarkup = inline.ToApi()
} else if reply != nil {
msgConfig.ReplyMarkup = reply.ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
return ret, nil
cfg := inline.SendConfig(sid, bot).
WithName("page/inline")
ret = append(ret, cfg)
}
if p.Reply != nil {
cfg := reply.SendConfig(sid, bot).
WithName("page/reply")
ret = append(ret, cfg)
}
return ret
}
func (p *Page) Filter(
u *Update, msgs MessageMap,
) bool {
return false
}
func (p *Page) Serve(
c *Context, updates *UpdateChan,
) {
msgs, _ := c.Render(p)
inlineMsg := msgs["page/inline"]
if p.Action != nil {
c.Run(p.Action, c.Update)
}
subUpdates := c.RunWidgetBg(p.SubWidget)
defer subUpdates.Close()
inlineUpdates := c.RunWidgetBg(p.Inline)
defer inlineUpdates.Close()
replyUpdates := c.RunWidgetBg(p.Reply)
defer replyUpdates.Close()
subFilter, subFilterOk := p.SubWidget.(Filterer)
for u := range updates.Chan() {
switch {
case !p.Inline.Filter(u, MessageMap{"": inlineMsg}) :
inlineUpdates.Send(u)
case !p.Reply.Filter(u, msgs) :
replyUpdates.Send(u )
case p.SubWidget != nil :
if subFilterOk {
if subFilter.Filter(u, msgs) {
subUpdates.Send(u)
}
} else {
msgConfig.ReplyMarkup = NewReply().
WithRemove(true).
ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
return ret, nil
subUpdates.Send(u)
}
default:
}
}
ret = append(ret, &SendConfig{Message: &msgConfig})
}
// Screen text and reply keyboard.
if reply != nil {
msgConfig := tgbotapi.NewMessage(cid, ">")
msgConfig.ReplyMarkup = reply.ToApi()
ret = append(ret, &SendConfig{
Message: &msgConfig,
})
} else {
// Removing keyboard if there is none.
msgConfig := tgbotapi.NewMessage(cid, ">")
msgConfig.ReplyMarkup = NewReply().
WithRemove(true).
ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
}
return ret, nil
}

View file

@ -10,7 +10,7 @@ type context struct {
Session *Session
// To reach the bot abilities inside callbacks.
Bot *Bot
skippedUpdates chan *Update
skippedUpdates *UpdateChan
// Current screen ID.
screenId, prevScreenId ScreenId
}
@ -20,14 +20,12 @@ type context struct {
// handling functions. Is provided to Act() function always.
// Goroutie function to handle each user.
func (c *context) handleUpdateChan(updates chan *Update) {
func (c *Context) Serve(updates *UpdateChan) {
beh := c.Bot.behaviour
if beh.Init != nil {
c.run(beh.Init, nil)
c.Run(beh.Init, c.Update)
}
beh.Root.Serve(&Context{
context: c,
}, updates)
beh.Root.Serve(c, updates)
}
@ -53,14 +51,12 @@ func (c *Context) Run(a Action, u *Update) {
// Skip the update sending it down to
// the underlying widget.
func (c *Context) Skip(u *Update) {
if c.skippedUpdates != nil {
c.skippedUpdates <- u
}
c.skippedUpdates.Send(u)
}
// Renders the Renedrable object to the side of client
// and returns the messages it sent.
func (c *Context) Render(v Renderable) ([]*Message, error) {
func (c *Context) Render(v Renderable) (MessageMap, error) {
return c.Bot.Render(c.Session.Id, v)
}
@ -71,13 +67,7 @@ func (c *Context) Send(v Sendable) (*Message, error) {
// Sends the formatted with fmt.Sprintf message to the user.
func (c *Context) Sendf(format string, v ...any) (*Message, error) {
msg, err := c.Send(NewMessage(
c.Session.Id, fmt.Sprintf(format, v...),
))
if err != nil {
return nil, err
}
return msg, err
return c.Send(NewMessage(fmt.Sprintf(format, v...)))
}
// Interface to interact with the user.
@ -130,10 +120,8 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
c.screenId = screenId
// Making the new channel for the widget.
if c.skippedUpdates != nil {
close(c.skippedUpdates)
}
c.skippedUpdates = make(chan *Update)
c.skippedUpdates.Close()
c.skippedUpdates = NewUpdateChan()
if screen.Widget != nil {
// Running the widget if the screen has one.
go func() {
@ -142,6 +130,8 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
Update: c.Update,
Args: args,
}, c.skippedUpdates)
c.skippedUpdates.Close()
}()
} else {
panic("no widget defined for the screen")
@ -149,3 +139,7 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
return nil
}
func (c *Context) ChangeToPrevScreen() {
c.ChangeScreen(c.PrevScreenId())
}

View file

@ -11,9 +11,6 @@ type Screen struct {
Id ScreenId
// The widget to run when reaching the screen.
Widget Widget
// Needs implementation later.
Dynamic DynamicWidget
}
// Map structure for the screens.
@ -27,8 +24,3 @@ func NewScreen(id ScreenId, widget Widget) *Screen {
}
}
func (s *Screen) WithDynamic(dynamic DynamicWidget) *Screen {
s.Dynamic = dynamic
return s
}

View file

@ -10,25 +10,38 @@ type Image any
// Implementing the interface lets the
// value to be sent.
type Sendable interface {
SendConfig(SessionId, *Bot) (*SendConfig, error)
SendConfig(SessionId, *Bot) *SendConfig
}
type Renderable interface {
Render(SessionId, *Bot) ([]*SendConfig, error)
Render(SessionId, *Bot) ([]*SendConfig)
}
type Errorer interface {
Err() error
}
// The type is used as an endpoint to send messages
// via bot.Send .
type SendConfig struct {
// Simple message with text.
// to add text use lower image
// or see the ParseMode for tgbotapi .
// The name will be used to store
// the message in the map.
Name string
// Message with text and keyboard.
Message *tgbotapi.MessageConfig
// The image to be sent.
Image *tgbotapi.PhotoConfig
Error error
}
func (cfg *SendConfig) WithName(name string) *SendConfig {
cfg.Name = name
return cfg
}
type MessageMap map[string] *Message
// Convert to the bot.Api.Send format.
func (config *SendConfig) ToApi() tgbotapi.Chattable {
if config.Message != nil {

View file

@ -14,9 +14,6 @@ func (si SessionId) ToApi() int64 {
type Session struct {
// Id of the chat of the user.
Id SessionId
// True if the session started.
// (got the '/start' command.
started bool
// Custom value for each user.
Data any
}

View file

@ -1,7 +1,7 @@
package tg
import (
//tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// Implementing the interface provides
@ -12,7 +12,80 @@ type Widget interface {
// widget MUST end its work.
// Mostly made by looping over the
// updates range.
Serve(*Context, chan *Update) error
Serve(*Context, *UpdateChan)
}
// Needs implementation.
// Behaviour can be the root widget or something like
// that.
type RootWidget interface {
Widget
}
// Implementing the interface provides way
// to know exactly what kind of updates
// the widget needs.
type Filterer interface {
// Return true if should filter the update
// and not send it inside the widget.
Filter(*Update, MessageMap) bool
}
// The type represents general update channel.
type UpdateChan struct {
chn chan *Update
}
// Return new update channel.
func NewUpdateChan() *UpdateChan {
ret := &UpdateChan{}
ret.chn = make(chan *Update)
return ret
}
func (updates *UpdateChan) Chan() chan *Update {
return updates.chn
}
// Send an update to the channel.
func (updates *UpdateChan) Send(u *Update) {
if updates != nil && updates.chn == nil {
return
}
updates.chn <- u
}
// Read an update from the channel.
func (updates *UpdateChan) Read() *Update {
if updates == nil || updates.chn == nil {
return nil
}
return <-updates.chn
}
// Returns true if the channel is closed.
func (updates *UpdateChan) Closed() bool {
return updates.chn == nil
}
// Close the channel. Used in defers.
func (updates *UpdateChan) Close() {
if updates == nil || updates.chn == nil {
return
}
close(updates.chn)
updates.chn = nil
}
func (c *Context) RunWidgetBg(widget Widget) *UpdateChan {
if widget == nil {
return nil
}
updates := NewUpdateChan()
go widget.Serve(c, updates)
return updates
}
// Implementing the interface provides
@ -22,9 +95,188 @@ type DynamicWidget interface {
// The function that implements the Widget
// interface.
type WidgetFunc func(*Context, chan *Update) error
type WidgetFunc func(*Context, *UpdateChan)
func (wf WidgetFunc) Serve(c *Context, updates chan *Update) error {
return wf(c, updates)
func (wf WidgetFunc) Serve(c *Context, updates *UpdateChan) {
wf(c, updates)
}
func (wf WidgetFunc) Filter(
u *Update,
msgs ...*Message,
) bool {
return false
}
// The type implements message with an inline keyboard.
type InlineKeyboardWidget struct {
Text string
*InlineKeyboard
}
// The type implements dynamic inline keyboard widget.
// Aka message with inline keyboard.
func NewInlineKeyboardWidget(
text string,
inline *InlineKeyboard,
) *InlineKeyboardWidget {
ret := &InlineKeyboardWidget{}
ret.Text = text
ret.InlineKeyboard = inline
return ret
}
func (widget *InlineKeyboardWidget) SendConfig(
sid SessionId,
bot *Bot,
) (*SendConfig) {
var text string
if widget.Text != "" {
text = widget.Text
} else {
text = ">"
}
msgConfig := tgbotapi.NewMessage(sid.ToApi(), text)
msgConfig.ReplyMarkup = widget.ToApi()
ret := &SendConfig{}
ret.Message = &msgConfig
return ret
}
func (widget *InlineKeyboardWidget) Serve(
c *Context,
updates *UpdateChan,
) {
for u := range updates.Chan() {
var act Action
if u.CallbackQuery == nil {
continue
}
cb := tgbotapi.NewCallback(
u.CallbackQuery.ID,
u.CallbackQuery.Data,
)
data := u.CallbackQuery.Data
_, err := c.Bot.Api.Request(cb)
if err != nil {
//return err
continue
}
btns := widget.ButtonMap()
btn, ok := btns[data]
if !ok {
continue
}
if btn != nil {
act = btn.Action
} else if widget.Action != nil {
act = widget.Action
}
c.Run(act, u)
}
}
func (widget *InlineKeyboardWidget) Filter(
u *Update,
msgs MessageMap,
) bool {
if widget == nil {
return true
}
if u.CallbackQuery == nil || len(msgs) < 1 {
return true
}
inlineMsg, inlineOk := msgs[""]
if inlineOk {
if u.CallbackQuery.Message.MessageID !=
inlineMsg.MessageID {
return true
}
}
return false
}
// The type implements dynamic reply keyboard widget.
type ReplyKeyboardWidget struct {
Text string
*ReplyKeyboard
}
// Returns new empty reply keyboard widget.
func NewReplyKeyboardWidget(
text string,
reply *ReplyKeyboard,
) *ReplyKeyboardWidget {
ret := &ReplyKeyboardWidget{}
ret.Text = text
ret.ReplyKeyboard = reply
return ret
}
func (widget *ReplyKeyboardWidget) SendConfig(
sid SessionId,
bot *Bot,
) (*SendConfig) {
var text string
if widget.Text != "" {
text = widget.Text
} else {
text = ">"
}
msgConfig := tgbotapi.NewMessage(sid.ToApi(), text)
msgConfig.ReplyMarkup = widget.ToApi()
ret := &SendConfig{}
ret.Message = &msgConfig
return ret
}
func (widget *ReplyKeyboardWidget) Filter(
u *Update,
msgs MessageMap,
) bool {
if widget == nil {
return true
}
if u.Message == nil {
return true
}
_, ok := widget.ButtonMap()[u.Message.Text]
if !ok {
return true
}
return false
}
func (widget *ReplyKeyboardWidget) Serve(
c *Context,
updates *UpdateChan,
) {
for u := range updates.Chan() {
var btn *Button
text := u.Message.Text
btns := widget.ButtonMap()
btn, ok := btns[text]
if !ok {
if u.Message.Location != nil {
for _, b := range btns {
if b.SendLocation {
btn = b
ok = true
}
}
}
}
if btn != nil {
c.Run(btn.Action, u)
}
}
}