diff --git a/cmd/test/main.go b/cmd/test/main.go index e24f9eb..27c5615 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -20,13 +20,11 @@ type SessionData struct { } type MutateMessageWidget struct { - *tg.Compo Mutate func(string) string } func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget { ret := &MutateMessageWidget{} - ret.Compo = tg.NewCompo() ret.Mutate = fn return ret } @@ -104,53 +102,55 @@ WithInitFunc(func(c *tg.Context) { }).WithRootNode(tg.NewRootNode( // The "/" widget. tg.RenderFunc(func(c *tg.Context) tg.UI {return tg.UI { - - tg.NewKeyboard().Row( + tg.NewMessage(fmt.Sprintf( + fmt.Sprint( + "Hello, %s!\n", + "The testing bot started!\n", + "You can see the basics of usage in the ", + "cmd/test/main.go file!", + ), + c.SentFrom().UserName, + )).Inline( + tg.NewKeyboard().Row( tg.NewButton("GoT Github page"). WithUrl("https://github.com/mojosa-software/got"), - ).Inline().Compo( - fmt.Sprintf( - fmt.Sprint( - "Hello, %s!\n", - "The testing bot started!\n", - "You can see the basics of usage in the ", - "cmd/test/main.go file!", - ), - c.SentFrom().UserName, - ), - ), - - tg.NewKeyboard().Row( - tg.NewButton("Inc/Dec").Go("/inc-dec"), - ).Row( - tg.NewButton("Mutate messages").Go("/mutate-messages"), - ).Row( - tg.NewButton("Send location").Go("/send-location"), - ).Reply().Compo( - "Choose the point of your interest", - ), + ).Inline(), + ), + tg.NewMessage("Choose your interest").Reply( + tg.NewKeyboard().Row( + tg.NewButton("Inc/Dec").Go("/inc-dec"), + ).Row( + tg.NewButton("Mutate messages").Go("/mutate-messages"), + ).Row( + tg.NewButton("Send location").Go("/send-location"), + ).Reply(), + ), }}), tg.NewNode( "mutate-messages", tg.RenderFunc(func(c *tg.Context) tg.UI { return tg.UI{ - tg.NewKeyboard().Row( - tg.NewButton("Upper case").Go("upper-case"), - tg.NewButton("Lower case").Go("lower-case"), - ).Row( - backButton, - ).Reply().Compo( + tg.NewMessage( "Choose the function to mutate string", + ).Reply( + tg.NewKeyboard().Row( + tg.NewButton("Upper case").Go("upper-case"), + tg.NewButton("Lower case").Go("lower-case"), + ).Row( + backButton, + ).Reply(), ), } }), tg.NewNode( "upper-case", tg.RenderFunc(func(c *tg.Context) tg.UI { return tg.UI{ - backKeyboard.Reply().Compo( - "Type a string and the bot will convert it to upper case", - ), + tg.NewMessage( + "Type a string and the bot will convert it to upper case", + ).Reply( + backKeyboard.Reply(), + ), NewMutateMessageWidget(strings.ToUpper), } }), @@ -158,10 +158,11 @@ WithInitFunc(func(c *tg.Context) { tg.NewNode( "lower-case", tg.RenderFunc(func(c *tg.Context) tg.UI { return tg.UI{ - backKeyboard.Reply(). - Compo( - "Type a string and the bot will convert it to lower case", - ), + tg.NewMessage( + "Type a string and the bot will convert it to lower case", + ).Reply( + backKeyboard.Reply(), + ), NewMutateMessageWidget(strings.ToLower), } }), @@ -172,10 +173,12 @@ WithInitFunc(func(c *tg.Context) { "inc-dec", tg.RenderFunc(func(c *tg.Context) tg.UI { d := ExtractSessionData(c) return tg.UI{ - incDecKeyboard.Reply().Compo(fmt.Sprintf( + tg.NewMessage(fmt.Sprintf( "Press the buttons to increment and decrement.\n" + "Current counter value = %d", d.Counter, - )), + )).Reply( + incDecKeyboard.Reply(), + ), } }), ), @@ -183,19 +186,25 @@ WithInitFunc(func(c *tg.Context) { tg.NewNode( "send-location", tg.RenderFunc(func(c *tg.Context) tg.UI { return tg.UI { + tg.NewMessage( + "Press the button to display your counter", + ).Inline( tg.NewKeyboard().Row( - tg.NewButton( - "Check", - ).WithData( - "check", - ).WithAction(tg.Func(func(c *tg.Context) { - d := ExtractSessionData(c) - c.Sendf("Counter = %d", d.Counter) - })), - ).Inline().Compo("Press the button to display your counter"), + tg.NewButton( + "Check", + ).WithData( + "check", + ).WithAction(tg.Func(func(c *tg.Context) { + d := ExtractSessionData(c) + c.Sendf("Counter = %d", d.Counter) + })), + ).Inline(), + ), - sendLocationKeyboard.Compo( + tg.NewMessage( "Press the button to send your location!", + ).Reply( + sendLocationKeyboard, ), } }), @@ -243,19 +252,6 @@ WithPreStart(tg.Func(func(c *tg.Context){ })), )) -var gBeh = tg.NewGroupBehaviour(). - InitFunc(func(c *tg.GC) { - }). - WithCommands( - tg.NewGroupCommand("hello").ActionFunc(func(c *tg.GC) { - c.Sendf("Hello, World!") - }), - tg.NewGroupCommand("mycounter").ActionFunc(func(c *tg.GC) { - d := c.Session().Data.(*SessionData) - c.Sendf("Your counter value is %d", d.Counter) - }), - ) - func main() { log.Println(beh.Screens) token := os.Getenv("BOT_TOKEN") diff --git a/tg/bot.go b/tg/bot.go index b2a804c..bd6f9ee 100644 --- a/tg/bot.go +++ b/tg/bot.go @@ -49,7 +49,8 @@ func (bot *Bot) Debug(debug bool) *Bot { } // Send the Renderable to the specified session client side. -// Can be used for both group and private sessions. +// Can be used for both group and private sessions because +// SessionId represents both for chat IDs. func (bot *Bot) Send( sid SessionId, v Sendable, args ...any, ) (*Message, error) { @@ -61,17 +62,7 @@ func (bot *Bot) Send( c := &Context{ context: ctx, } - - config := v.SendConfig(c.WithArg(c.MakeArg(args))) - if config.Error != nil { - return nil, config.Error - } - - msg, err := bot.Api.Send(config.ToApi()) - if err != nil { - return nil, err - } - return &msg, nil + return c.Bot.Send(c.Session.Id, v) } /*func (bot *Bot) Render( diff --git a/tg/command.go b/tg/command.go index 1fd1c45..97ce45a 100644 --- a/tg/command.go +++ b/tg/command.go @@ -107,14 +107,17 @@ type CommandCompo struct { } // Returns new empty CommandCompo. -func NewCommandCompo() *CommandCompo { - ret := &CommandCompo{} - ret.Commands = make(CommandMap) +func NewCommandCompo(cmds ...*Command) *CommandCompo { + ret := (&CommandCompo{}).WithCommands(cmds...) + //ret.Commands = make(CommandMap) return ret } // Set the commands to handle. func (w *CommandCompo) WithCommands(cmds ...*Command) *CommandCompo { + if w.Commands == nil { + w.Commands = make(CommandMap) + } for _, cmd := range cmds { if cmd.Name == "" { panic("empty command name") diff --git a/tg/compo.go b/tg/compo.go index e54c86b..d926bc8 100644 --- a/tg/compo.go +++ b/tg/compo.go @@ -29,19 +29,3 @@ type Component interface { Server } -// The type to embed into potential components. -// Implements empty versions of interfaces. -type Compo struct{ - *Message -} - -func NewCompo() *Compo { - return &Compo{} -} - -// Defalut setting message -func (compo *Compo) SetMessage(msg *Message) { compo.Message = msg } -func (compo *Compo) GetMessage() *Message { return compo.Message } -// Default non filtering filter. Always returns false. -func (compo *Compo) Filter(_ *Update) bool {return false} - diff --git a/tg/context.go b/tg/context.go index d814b6d..4a2a4d6 100644 --- a/tg/context.go +++ b/tg/context.go @@ -81,18 +81,30 @@ func (c *Context) Skip(u *Update) { // Sends to the Sendable object. func (c *Context) Send(v Sendable) (*Message, error) { - return c.Bot.Send(c.Session.Id, v) + config := v.SendConfig(c) + if config.Error != nil { + return nil, config.Error + } + + msg, err := c.Bot.Api.Send(config.ToApi()) + if err != nil { + return nil, err + } + return &msg, nil } -// Sends the formatted with fmt.Sprintf message to the user. +// Sends the formatted with fmt.Sprintf message to the user +// using default Markdown parsing format. func (c *Context) Sendf(format string, v ...any) (*Message, error) { return c.Send(NewMessage(fmt.Sprintf(format, v...))) } +// Same as Sendf but uses Markdown 2 format for parsing. func (c *Context) Sendf2(format string, v ...any) (*Message, error) { return c.Send(NewMessage(fmt.Sprintf(format, v...)).MD2()) } +// Same as Sendf but uses HTML format for parsing. func (c *Context) SendfHTML(format string, v ...any) (*Message, error) { return c.Send(NewMessage(fmt.Sprintf(format, v...)).HTML()) } @@ -227,7 +239,7 @@ func (c *Context) RunWidget(widget Widget, args ...any) *UpdateChan { pth := c.Path() compos := widget.Render(c.WithArg(c.MakeArg(args))) // Leave if changed path. - if pth != c.Path() { + if compos == nil || pth != c.Path() { return nil } chns := make([]*UpdateChan, len(compos)) @@ -237,13 +249,27 @@ func (c *Context) RunWidget(widget Widget, args ...any) *UpdateChan { ret := NewUpdateChan() go func() { + ln := len(compos) + UPDATE: for u := range ret.Chan() { + if u == nil { + break + } + cnt := 0 for i, compo := range compos { chn := chns[i] + if chn.Closed() { + cnt++ + continue + } if !compo.Filter(u) { chn.Send(u) + continue UPDATE } } + if cnt == ln { + break + } } ret.Close() for _, chn := range chns { diff --git a/tg/file.go b/tg/file.go index e0d8e1d..20bdacb 100644 --- a/tg/file.go +++ b/tg/file.go @@ -23,17 +23,18 @@ var ( ) type File struct { - *Compo + *MessageCompo path string typ FileType caption string } func NewFile(path string) *File { - ret := &File{ - path: path, - } - ret.Compo = NewCompo() + ret := &File{} + + ret.MessageCompo = NewMessage("") + ret.path = path + return ret } diff --git a/tg/inline.go b/tg/inline.go index 1cbfb9a..174a734 100644 --- a/tg/inline.go +++ b/tg/inline.go @@ -23,49 +23,41 @@ func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup { return tgbotapi.NewInlineKeyboardMarkup(rows...) } -// Transform the keyboard to widget with the specified text. -func (kbd *Inline) Compo(text string) *InlineCompo { - ret := &InlineCompo{} - ret.Inline = kbd - ret.Text = text - ret.Compo = NewCompo() - return ret -} - // The type implements message with an inline keyboard. type InlineCompo struct { - *Compo - Text string + *MessageCompo *Inline } // Implementing the Sendable interface. -func (widget *InlineCompo) SendConfig( +func (compo *InlineCompo) SendConfig( c *Context, ) (*SendConfig) { - var text string - if widget.Text != "" { - text = widget.Text - } else { - text = ">" + + sendConfig := compo.MessageCompo.SendConfig(c) + sendConfig.Message.ReplyMarkup = compo.Inline.ToApi() + + return sendConfig +} + +// Implementing the Filterer interface. +func (compo *InlineCompo) Filter(u *Update) bool { + if compo == nil || u.CallbackQuery == nil { + return true } - sid := c.Session.Id.ToApi() - msgConfig := tgbotapi.NewMessage(sid, text) - msgConfig.ReplyMarkup = widget.ToApi() + if u.CallbackQuery.Message.MessageID != + compo.Message.MessageID { + return true + } - ret := &SendConfig{} - ret.Message = &msgConfig - return ret + return false } // Implementing the Server interface. func (widget *InlineCompo) Serve(c *Context) { for u := range c.Input() { var act Action - if u.CallbackQuery == nil { - continue - } cb := tgbotapi.NewCallback( u.CallbackQuery.ID, u.CallbackQuery.Data, @@ -92,17 +84,4 @@ func (widget *InlineCompo) Serve(c *Context) { } } -// Implementing the Filterer interface. -func (compo *InlineCompo) Filter(u *Update) bool { - if compo == nil || u.CallbackQuery == nil { - return true - } - - if u.CallbackQuery.Message.MessageID != - compo.Message.MessageID { - return true - } - - return false -} diff --git a/tg/message.go b/tg/message.go index 10dcbb8..6fd15e0 100644 --- a/tg/message.go +++ b/tg/message.go @@ -5,45 +5,87 @@ import ( ) // Simple text message type. -type MessageConfig struct { - Compo +type MessageCompo struct { + Message *Message ParseMode string Text string } +func (compo *MessageCompo) SetMessage(msg *Message) { + compo.Message = msg +} + // Return new message with the specified text. -func NewMessage(text string) *MessageConfig { - ret := &MessageConfig{} +func NewMessage(text string) *MessageCompo { + ret := &MessageCompo{} ret.Text = text ret.ParseMode = tgbotapi.ModeMarkdown return ret } -func (msg *MessageConfig) withParseMode(mode string) *MessageConfig{ +func (msg *MessageCompo) withParseMode(mode string) *MessageCompo { msg.ParseMode = mode return msg } // Set the default Markdown parsing mode. -func (msg *MessageConfig) MD() *MessageConfig { +func (msg *MessageCompo) MD() *MessageCompo { return msg.withParseMode(tgbotapi.ModeMarkdown) } -func (msg *MessageConfig) MD2() *MessageConfig { +// Set the Markdown 2 parsing mode. +func (msg *MessageCompo) MD2() *MessageCompo { return msg.withParseMode(tgbotapi.ModeMarkdownV2) } -func (msg *MessageConfig) HTML() *MessageConfig { +// Set the HTML parsing mode. +func (msg *MessageCompo) HTML() *MessageCompo { return msg.withParseMode(tgbotapi.ModeHTML) } -func (config *MessageConfig) SendConfig( +// Transform the message component into one with reply keyboard. +func (msg *MessageCompo) Inline(inline *Inline) *InlineCompo { + return &InlineCompo{ + Inline: inline, + MessageCompo: msg, + } +} + +// Transform the message component into one with reply keyboard. +func (msg *MessageCompo) Reply(reply *Reply) *ReplyCompo { + return &ReplyCompo{ + Reply: reply, + MessageCompo: msg, + } +} + +func (config *MessageCompo) SendConfig( c *Context, ) (*SendConfig) { - var ret SendConfig - msg := tgbotapi.NewMessage(c.Session.Id.ToApi(), config.Text) + var ( + ret SendConfig + text string + ) + + if config.Text == "" { + text = ">" + } else { + text = config.Text + } + + msg := tgbotapi.NewMessage(c.Session.Id.ToApi(), text) ret.Message = &msg ret.Message.ParseMode = config.ParseMode + return &ret } + +// Empty serving to use messages in rendering. +func (compo *MessageCompo) Serve(c *Context) { +} + +func (compo *MessageCompo) Filter(_ *Update) bool { + // Skip everything + return true +} diff --git a/tg/reply.go b/tg/reply.go index 7727dde..2ea4183 100644 --- a/tg/reply.go +++ b/tg/reply.go @@ -50,19 +50,9 @@ func (kbd *Reply) ToApi() any { return tgbotapi.NewReplyKeyboard(rows...) } -// Transform the keyboard to widget with the specified text. -func (kbd *Reply) Compo(text string) *ReplyCompo { - ret := &ReplyCompo{} - ret.Reply = kbd - ret.Text = text - ret.Compo = NewCompo() - return ret -} - // The type implements reply keyboard widget. type ReplyCompo struct { - *Compo - Text string + *MessageCompo *Reply } @@ -70,27 +60,9 @@ type ReplyCompo struct { func (compo *ReplyCompo) SendConfig( c *Context, ) (*SendConfig) { - sid := c.Session.Id.ToApi() - if compo == nil { - msgConfig := tgbotapi.NewMessage(sid, ">") - msgConfig.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true) - return &SendConfig{ - Message: &msgConfig, - } - } - var text string - if compo.Text != "" { - text = compo.Text - } else { - text = ">" - } - - msgConfig := tgbotapi.NewMessage(sid, text) - msgConfig.ReplyMarkup = compo.ToApi() - - ret := &SendConfig{} - ret.Message = &msgConfig - return ret + sendConfig := compo.MessageCompo.SendConfig(c) + sendConfig.Message.ReplyMarkup = compo.Reply.ToApi() + return sendConfig } func (compo *ReplyCompo) Filter( @@ -134,4 +106,3 @@ func (compo *ReplyCompo) Serve(c *Context) { } } - diff --git a/tg/send.go b/tg/send.go index 5379cf7..07b471c 100644 --- a/tg/send.go +++ b/tg/send.go @@ -12,7 +12,6 @@ type MessageId int64 type Sendable interface { SendConfig(*Context) (*SendConfig) SetMessage(*Message) - GetMessage() *Message } type Errorer interface {