Implemented reading in commands as widget.

This commit is contained in:
Andrey Parhomenko 2023-09-11 13:37:04 +03:00
parent 6c6b041133
commit 36fa167549
4 changed files with 237 additions and 202 deletions

View file

@ -16,6 +16,7 @@ type Command struct {
Name CommandName Name CommandName
Description string Description string
Action *action Action *action
Widget Widget
} }
type CommandMap map[CommandName]*Command type CommandMap map[CommandName]*Command
@ -34,6 +35,15 @@ func (c *Command) ActionFunc(af ActionFunc) *Command {
return c.WithAction(af) return c.WithAction(af)
} }
func (c *Command) WithWidget(w Widget) *Command {
c.Widget = w
return c
}
func (c *Command) WidgetFunc(fn WidgetFunc) *Command {
return c.WithWidget(fn)
}
func (c *Command) ToApi() tgbotapi.BotCommand { func (c *Command) ToApi() tgbotapi.BotCommand {
ret := tgbotapi.BotCommand{} ret := tgbotapi.BotCommand{}
ret.Command = string(c.Name) ret.Command = string(c.Name)

202
tg/page.go Normal file
View file

@ -0,0 +1,202 @@
package tg
import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// The basic widget to provide keyboard functionality
// without implementing much.
type Page struct {
Text string
SubWidget Widget
Inline *InlineKeyboard
Reply *ReplyKeyboard
Action Action
}
// Return new page with the specified text.
func NewPage(text string) *Page {
ret := &Page{}
ret.Text = text
return ret
}
// Set the inline keyboard.
func (p *Page) WithInline(inline *InlineKeyboard) *Page {
p.Inline = inline
return p
}
// Set the reply keyboard.
func (p *Page) WithReply(reply *ReplyKeyboard) *Page {
p.Reply = reply
return p
}
// Set the action to be run before serving.
func (p *Page) WithAction(a Action) *Page {
p.Action = a
return p
}
// Alias to with action to simpler define actions.
func (p *Page) ActionFunc(fn ActionFunc) *Page {
return p.WithAction(fn)
}
// Set the sub widget that will get the skipped
// updates.
func (p *Page) WithSub(sub Widget) *Page {
p.SubWidget = sub
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
}
}
if act != nil {
c.run(act, u)
}
}
return nil
}
func (s *Page) Render(
sid SessionId, bot *Bot,
) ([]*SendConfig, error) {
cid := sid.ToApi()
reply := s.Reply
inline := s.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 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
} else {
msgConfig.ReplyMarkup = NewReply().
WithRemove(true).
ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
return ret, nil
}
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

@ -26,6 +26,7 @@ func (c *context) handleUpdateChan(updates chan *Update) {
if beh.Init != nil { if beh.Init != nil {
c.run(beh.Init, nil) c.run(beh.Init, nil)
} }
var cmdUpdates chan *Update
for u := range updates { for u := range updates {
// The part is added to implement custom update handling. // The part is added to implement custom update handling.
if !session.started { if !session.started {
@ -59,14 +60,32 @@ func (c *context) handleUpdateChan(updates chan *Update) {
if cmd.Action != nil { if cmd.Action != nil {
c.run(cmd.Action, u) c.run(cmd.Action, u)
} }
if cmd.Widget != nil {
if cmdUpdates != nil {
close(cmdUpdates)
}
cmdUpdates = make(chan *Update)
go func() {
cmd.Widget.Serve(
&Context{context: c, Update: u},
cmdUpdates,
)
close(cmdUpdates)
cmdUpdates = nil
}()
}
} else { } else {
// Some usage. // Some usage.
} }
continue continue
} }
// The standard thing - send messages to widgets. // The standard thing - send messages to widgets.
c.widgetUpdates <- u if cmdUpdates != nil {
cmdUpdates <- u
} else {
c.widgetUpdates <- u
}
} }
} }

View file

@ -1,7 +1,7 @@
package tg package tg
import ( 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 // Implementing the interface provides
@ -22,205 +22,9 @@ type DynamicWidget interface {
// The function that implements the Widget // The function that implements the Widget
// interface. // interface.
type WidgetFunc func(*Context, chan *Update) type WidgetFunc func(*Context, chan *Update) error
func (wf WidgetFunc) Serve(c *Context, updates chan *Update){ func (wf WidgetFunc) Serve(c *Context, updates chan *Update) error {
wf(c, updates) return wf(c, updates)
}
// The basic widget to provide keyboard functionality
// without implementing much.
type Page struct {
Text string
SubWidget Widget
Inline *InlineKeyboard
Reply *ReplyKeyboard
Action Action
}
// Return new page with the specified text.
func NewPage(text string) *Page {
ret := &Page{}
ret.Text = text
return ret
}
// Set the inline keyboard.
func (p *Page) WithInline(inline *InlineKeyboard) *Page {
p.Inline = inline
return p
}
// Set the reply keyboard.
func (p *Page) WithReply(reply *ReplyKeyboard) *Page {
p.Reply = reply
return p
}
// Set the action to be run before serving.
func (p *Page) WithAction(a Action) *Page {
p.Action = a
return p
}
// Alias to with action to simpler define actions.
func (p *Page) ActionFunc(fn ActionFunc) *Page {
return p.WithAction(fn)
}
// Set the sub widget that will get the skipped
// updates.
func (p *Page) WithSub(sub Widget) *Page {
p.SubWidget = sub
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
}
}
if act != nil {
c.run(act, u)
}
}
return nil
}
func (s *Page) Render(
sid SessionId, bot *Bot,
) ([]*SendConfig, error) {
cid := sid.ToApi()
reply := s.Reply
inline := s.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 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
} else {
msgConfig.ReplyMarkup = NewReply().
WithRemove(true).
ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
return ret, nil
}
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
} }