diff --git a/tg/command.go b/tg/command.go index 27d7ff9..c333a16 100644 --- a/tg/command.go +++ b/tg/command.go @@ -16,6 +16,7 @@ type Command struct { Name CommandName Description string Action *action + Widget Widget } type CommandMap map[CommandName]*Command @@ -34,6 +35,15 @@ func (c *Command) ActionFunc(af ActionFunc) *Command { 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 { ret := tgbotapi.BotCommand{} ret.Command = string(c.Name) diff --git a/tg/page.go b/tg/page.go new file mode 100644 index 0000000..fed97d9 --- /dev/null +++ b/tg/page.go @@ -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 +} + diff --git a/tg/private.go b/tg/private.go index 5c6e97c..1f05f6e 100644 --- a/tg/private.go +++ b/tg/private.go @@ -26,6 +26,7 @@ func (c *context) handleUpdateChan(updates chan *Update) { if beh.Init != nil { c.run(beh.Init, nil) } + var cmdUpdates chan *Update for u := range updates { // The part is added to implement custom update handling. if !session.started { @@ -59,14 +60,32 @@ func (c *context) handleUpdateChan(updates chan *Update) { if cmd.Action != nil { 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 { // Some usage. } continue - } + } // The standard thing - send messages to widgets. - c.widgetUpdates <- u + if cmdUpdates != nil { + cmdUpdates <- u + } else { + c.widgetUpdates <- u + } } } diff --git a/tg/widget.go b/tg/widget.go index f7c3f46..133674b 100644 --- a/tg/widget.go +++ b/tg/widget.go @@ -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 @@ -22,205 +22,9 @@ type DynamicWidget interface { // The function that implements the Widget // interface. -type WidgetFunc func(*Context, chan *Update) +type WidgetFunc func(*Context, chan *Update) error -func (wf WidgetFunc) Serve(c *Context, updates chan *Update){ - 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 +func (wf WidgetFunc) Serve(c *Context, updates chan *Update) error { + return wf(c, updates) }