diff --git a/cmd/test/main.go b/cmd/test/main.go index 5257e83..fbb89f7 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -20,7 +20,7 @@ var ( startScreenButton = tg.NewButton("🏠 To the start screen"). ScreenChange("start") - incDecKeyboard = tg.NewKeyboard("").Row( + incDecKeyboard = tg.NewInline().Row( tg.NewButton("+").ActionFunc(func(c *tg.Context) { d := c.Session.Value.(*UserData) d.Counter++ @@ -35,7 +35,7 @@ var ( startScreenButton, ) - navKeyboard = tg.NewKeyboard("Choose your interest"). + navKeyboard = tg.NewReply(). WithOneTime(true). Row( tg.NewButton("Inc/Dec").ScreenChange("inc/dec"), @@ -46,7 +46,7 @@ var ( tg.NewButton("Send location").ScreenChange("send-location"), ) - sendLocationKeyboard = tg.NewKeyboard("Press the button to send your location"). + sendLocationKeyboard = tg.NewReply(). Row( tg.NewButton("Send location"). WithSendLocation(true). @@ -64,10 +64,10 @@ var ( l.Heading, ) } else { - _, err = c.Send("Somehow wrong location was sent") + _, err = c.Sendf("Somehow wrong location was sent") } if err != nil { - c.Send(err) + c.Sendf("%q", err) } }), ).Row( @@ -75,7 +75,7 @@ var ( ) // The keyboard to return to the start screen. - navToStartKeyboard = tg.NewKeyboard("").Row( + navToStartKeyboard = tg.NewReply().Row( startScreenButton, ) ) @@ -87,7 +87,7 @@ var beh = tg.NewBehaviour(). }). // On any message update before the bot created session. WithPreStartFunc(func(c *tg.Context){ - c.Send("Please, use the /start command to start the bot") + c.Sendf("Please, use the /start command to start the bot") }).WithScreens( tg.NewScreen("start"). WithText( @@ -96,10 +96,10 @@ var beh = tg.NewBehaviour(). " understand of how the API works, so just"+ " horse around a bit to guess everything out"+ " by yourself!", - ).WithKeyboard(navKeyboard). + ).WithReply(navKeyboard). // The inline keyboard with link to GitHub page. - WithIKeyboard( - tg.NewKeyboard("istart").Row( + WithInline( + tg.NewInline().Row( tg.NewButton("GoT Github page"). WithUrl("https://github.com/mojosa-software/got"), ), @@ -112,7 +112,7 @@ var beh = tg.NewBehaviour(). "by saving the counter for each of users "+ "separately. ", ). - WithKeyboard(incDecKeyboard). + WithReply(&tg.ReplyKeyboard{Keyboard: incDecKeyboard.Keyboard}). // The function will be called when reaching the screen. ActionFunc(func(c *tg.Context) { d := c.Session.Value.(*UserData) @@ -121,19 +121,19 @@ var beh = tg.NewBehaviour(). tg.NewScreen("upper-case"). WithText("Type text and the bot will send you the upper case version to you"). - WithKeyboard(navToStartKeyboard). + WithReply(navToStartKeyboard). ActionFunc(mutateMessage(strings.ToUpper)), tg.NewScreen("lower-case"). WithText("Type text and the bot will send you the lower case version"). - WithKeyboard(navToStartKeyboard). + WithReply(navToStartKeyboard). ActionFunc(mutateMessage(strings.ToLower)), tg.NewScreen("send-location"). WithText("Send your location and I will tell where you are!"). - WithKeyboard(sendLocationKeyboard). - WithIKeyboard( - tg.NewKeyboard("").Row( + WithReply(sendLocationKeyboard). + WithInline( + tg.NewInline().Row( tg.NewButton("Check"). WithData("check"). ActionFunc(func(a *tg.Context) { @@ -151,12 +151,12 @@ var beh = tg.NewBehaviour(). tg.NewCommand("hello"). Desc("sends the 'Hello, World!' message back"). ActionFunc(func(c *tg.Context) { - c.Send("Hello, World!") + c.Sendf("Hello, World!") }), tg.NewCommand("read"). Desc("reads a string and sends it back"). ActionFunc(func(c *tg.Context) { - c.Send("Type some text:") + c.Sendf("Type some text:") msg, err := c.ReadTextMessage() if err != nil { return diff --git a/tg/beh.go b/tg/beh.go index f34c18e..29119ec 100644 --- a/tg/beh.go +++ b/tg/beh.go @@ -8,7 +8,6 @@ type Behaviour struct { PreStart *action Init *action Screens ScreenMap - Keyboards KeyboardMap Commands CommandMap } @@ -16,7 +15,6 @@ type Behaviour struct { func NewBehaviour() *Behaviour { return &Behaviour{ Screens: make(ScreenMap), - Keyboards: make(KeyboardMap), Commands: make(CommandMap), } } diff --git a/tg/bot.go b/tg/bot.go index fcb25e5..8fee2d3 100644 --- a/tg/bot.go +++ b/tg/bot.go @@ -3,7 +3,7 @@ package tg import ( "errors" - "fmt" + //"fmt" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) @@ -25,6 +25,7 @@ type Bot struct { sessions SessionMap groupSessions GroupSessionMap value any + } // Return the new bot with empty sessions and behaviour. @@ -58,25 +59,36 @@ func (bot *Bot) Debug(debug bool) *Bot { } func (bot *Bot) Send( - sid SessionId, v any, + sid SessionId, v Sendable, ) (*Message, error) { - sendable, ok := v.(Sendable) - if !ok { - cid := sid.ToApi() - str := tgbotapi.NewMessage( - cid, fmt.Sprint(v), - ) - msg, err := bot.Api.Send(str) - return &msg, err + config, err := v.SendConfig(sid, bot) + if err != nil { + return nil, err } - return sendable.Send(sid, bot) + msg, err := bot.Api.Send(config.ToApi()) + if err != nil { + return nil, err + } + return &msg, nil } func (bot *Bot) Render( sid SessionId, r Renderable, ) ([]*Message, error) { - return r.Render(sid, bot) + configs, err := r.Render(sid, bot) + if err != nil { + return []*Message{}, err + } + messages := []*Message{} + for _, config := range configs { + msg, err := bot.Api.Send(config.ToApi()) + if err != nil { + return messages, err + } + messages = append(messages, &msg) + } + return messages, nil } func (bot *Bot) GetSession( diff --git a/tg/file.go b/tg/file.go index 4f15ecf..95b2dd8 100644 --- a/tg/file.go +++ b/tg/file.go @@ -10,6 +10,7 @@ import ( "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) +type PhotoConfig = tgbotapi.PhotoConfig type FileType int const ( @@ -71,22 +72,22 @@ func (f *File) UploadData() (string, io.Reader, error) { func (f *File) SendData() string { return "" } -func (f *File) Send( +func (f *File) SendConfig( sid SessionId, bot *Bot, -) (*Message, error) { - var chattable tgbotapi.Chattable +) (*SendConfig, error) { + var config SendConfig cid := sid.ToApi() switch f.Type() { case ImageFileType: photo := tgbotapi.NewPhoto(cid, f) photo.Caption = f.caption - chattable = photo + + config.Image = &photo default: return nil, UnknownFileTypeErr } - msg, err := bot.Api.Send(chattable) - return &msg, err + return &config, nil } diff --git a/tg/keyboard.go b/tg/keyboard.go index 08a45ad..336e841 100644 --- a/tg/keyboard.go +++ b/tg/keyboard.go @@ -1,57 +1,48 @@ package tg import ( - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) -/* -var otherKeyboard = tgbotapi.NewReplyKeyboard( - tgbotapi.NewKeyboardButtonRow( - tgbotapi.NewKeyboardButton("a"), - tgbotapi.NewKeyboardButton("b"), - tgbotapi.NewKeyboardButton("c"), - ), - tgbotapi.NewKeyboardButtonRow( - tgbotapi.NewKeyboardButton("d"), - tgbotapi.NewKeyboardButton("e"), - tgbotapi.NewKeyboardButton("f"), - ), -)*/ - -type KeyboardId string - -// The type represents reply keyboard which -// is supposed to be showed on a Screen. +// The general keyboard type used both in Reply and Inline. type Keyboard struct { - // Text to be displayed with the keyboard. - Text string - // Rows to be displayed once the - // keyboard is sent. Rows []ButtonRow +} +type ReplyKeyboard struct { + Keyboard + // If true will be removed after one press. OneTime bool - Inline bool + // If true will remove the keyboard on send. + Remove bool } -type KeyboardMap map[KeyboardId]*Keyboard - -// Return the new reply keyboard with rows as specified. -func NewKeyboard(text string) *Keyboard { - return &Keyboard{ - Text: text, - } +// The keyboard to be emdedded into the messages. +type InlineKeyboard struct { + Keyboard } -func (kbd *Keyboard) TelegramMarkup() any { - if kbd.Inline { - return kbd.toTelegramInline() - } +func NewInline() *InlineKeyboard { + ret := &InlineKeyboard{} + return ret +} - return kbd.toTelegramReply() +func NewReply() *ReplyKeyboard { + ret := &ReplyKeyboard {} + return ret } // Adds a new button row to the current keyboard. -func (kbd *Keyboard) Row(btns ...*Button) *Keyboard { +func (kbd *InlineKeyboard) Row(btns ...*Button) *InlineKeyboard { + // For empty row. We do not need that. + if len(btns) < 1 { + return kbd + } + kbd.Rows = append(kbd.Rows, btns) + return kbd +} +// Adds a new button row to the current keyboard. +func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard { // For empty row. We do not need that. if len(btns) < 1 { return kbd @@ -61,10 +52,15 @@ func (kbd *Keyboard) Row(btns ...*Button) *Keyboard { } // Convert the Keyboard to the Telegram API type of reply keyboard. -func (kbd *Keyboard) toTelegramReply() apix.ReplyKeyboardMarkup { - rows := [][]apix.KeyboardButton{} +func (kbd *ReplyKeyboard) ToApi() any { + // Shades everything. + if kbd.Remove { + return tgbotapi.NewRemoveKeyboard(true) + } + + rows := [][]tgbotapi.KeyboardButton{} for _, row := range kbd.Rows { - buttons := []apix.KeyboardButton{} + buttons := []tgbotapi.KeyboardButton{} for _, button := range row { buttons = append(buttons, button.ToTelegram()) } @@ -72,37 +68,37 @@ func (kbd *Keyboard) toTelegramReply() apix.ReplyKeyboardMarkup { } if kbd.OneTime { - return apix.NewOneTimeReplyKeyboard(rows...) + return tgbotapi.NewOneTimeReplyKeyboard(rows...) } - return apix.NewReplyKeyboard(rows...) + return tgbotapi.NewReplyKeyboard(rows...) } -func (kbd *Keyboard) toTelegramInline() apix.InlineKeyboardMarkup { - rows := [][]apix.InlineKeyboardButton{} +func (kbd *InlineKeyboard) ToApi() tgbotapi.InlineKeyboardMarkup { + rows := [][]tgbotapi.InlineKeyboardButton{} for _, row := range kbd.Rows { - buttons := []apix.InlineKeyboardButton{} + buttons := []tgbotapi.InlineKeyboardButton{} for _, button := range row { buttons = append(buttons, button.ToTelegramInline()) } rows = append(rows, buttons) } - return apix.NewInlineKeyboardMarkup(rows...) + return tgbotapi.NewInlineKeyboardMarkup(rows...) } -func (kbd *Keyboard) WithOneTime(oneTime bool) *Keyboard { +func (kbd *ReplyKeyboard) WithRemove(remove bool) *ReplyKeyboard { + kbd.Remove = remove + return kbd +} + +func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard { kbd.OneTime = oneTime return kbd } -func (kbd *Keyboard) WithInline(inline bool) *Keyboard { - kbd.Inline = inline - return kbd -} - // Returns the map of buttons. Used to define the Action. -func (kbd *Keyboard) buttonMap() ButtonMap { +func (kbd Keyboard) buttonMap() ButtonMap { ret := make(ButtonMap) for _, vi := range kbd.Rows { for _, vj := range vi { diff --git a/tg/message.go b/tg/message.go new file mode 100644 index 0000000..0fd1d20 --- /dev/null +++ b/tg/message.go @@ -0,0 +1,51 @@ +package tg + +import ( + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type MessageConfig struct { + To SessionId + ReplyTo MessageId + Text string + Inline *InlineKeyboard + Reply *ReplyKeyboard +} + +func NewMessage(to SessionId, 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) { + 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() + } + + ret.Message = &msg + return &ret, nil +} diff --git a/tg/private.go b/tg/private.go index 8f6beae..909efb5 100644 --- a/tg/private.go +++ b/tg/private.go @@ -61,7 +61,7 @@ func (c *context) handleUpdateChan(updates chan *Update) { } } else { // Simple messages handling. - kbd := screen.Keyboard + kbd := screen.Reply if kbd == nil { if c.readingUpdate { c.updates <- u @@ -102,7 +102,7 @@ func (c *context) handleUpdateChan(updates chan *Update) { if err != nil { panic(err) } - kbd := screen.InlineKeyboard + kbd := screen.Inline if kbd == nil { if c.readingUpdate { c.updates <- u @@ -160,14 +160,16 @@ func (c *context) ReadTextMessage() (string, error) { return u.Message.Text, nil } -// Sends to the user specified text. -func (c *context) Send(v any) (*Message, error) { +// Sends to the Sendable object. +func (c *context) Send(v Sendable) (*Message, error) { return c.Bot.Send(c.Session.Id, v) } // Sends the formatted with fmt.Sprintf message to the user. func (c *context) Sendf(format string, v ...any) (*Message, error) { - msg, err := c.Send(fmt.Sprintf(format, v...)) + msg, err := c.Send(NewMessage( + c.Session.Id, fmt.Sprintf(format, v...), + )) if err != nil { return nil, err } @@ -224,7 +226,7 @@ func (c *Context) ChangeScreen(screenId ScreenId) error { screen := c.Bot.behaviour.Screens[screenId] c.prevScreen = c.curScreen c.curScreen = screen - screen.Render(c.Session.Id, c.Bot) + c.Bot.Render(c.Session.Id, screen) if screen.Action != nil { c.run(screen.Action, c.Update) } diff --git a/tg/screen.go b/tg/screen.go index 8a34e32..5012382 100644 --- a/tg/screen.go +++ b/tg/screen.go @@ -15,9 +15,9 @@ type Screen struct { // reached. Text string // The keyboard to be sent in the message part. - InlineKeyboard *Keyboard + Inline *InlineKeyboard // Keyboard to be displayed on the screen. - Keyboard *Keyboard + Reply *ReplyKeyboard // Action called on the reaching the screen. Action *action } @@ -38,17 +38,13 @@ func (s *Screen) WithText(text string) *Screen { return s } -func (s *Screen) WithInlineKeyboard(ikbd *Keyboard) *Screen { - s.InlineKeyboard = ikbd +func (s *Screen) WithInline(ikbd *InlineKeyboard) *Screen { + s.Inline= ikbd return s } -func (s *Screen) WithIKeyboard(ikbd *Keyboard) *Screen { - return s.WithInlineKeyboard(ikbd) -} - -func (s *Screen) WithKeyboard(kbd *Keyboard) *Screen { - s.Keyboard = kbd +func (s *Screen) WithReply(kbd *ReplyKeyboard) *Screen { + s.Reply = kbd return s } @@ -61,81 +57,54 @@ func (s *Screen) ActionFunc(a ActionFunc) *Screen { return s.WithAction(a) } -// Renders output of the screen only to the side of the user. func (s *Screen) Render( sid SessionId, bot *Bot, -) ([]*Message, error) { +) ([]*SendConfig, error) { cid := sid.ToApi() - kbd := s.Keyboard - iKbd := s.InlineKeyboard - - var ch [2]tgbotapi.Chattable + reply := s.Reply + inline := s.Inline + ret := []*SendConfig{} var txt string - - msgs := []*Message{} - // Screen text and inline keyboard. if s.Text != "" { txt = s.Text - } else if iKbd != nil { - if iKbd.Text != "" { - txt = iKbd.Text - } else { - // Default to send the keyboard. - txt = ">" - } + } else if inline != nil { + // Default to send the keyboard. + txt = ">" } if txt != "" { msgConfig := tgbotapi.NewMessage(cid, txt) - if iKbd != nil { - msgConfig.ReplyMarkup = iKbd.toTelegramInline() - } else if kbd != nil { - msgConfig.ReplyMarkup = kbd.toTelegramReply() - msg, err := bot.Api.Send(msgConfig) - if err != nil { - return msgs, err - } - msgs = append(msgs, &msg) - return msgs, nil + 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 = tgbotapi.NewRemoveKeyboard(true) - msg, err := bot.Api.Send(msgConfig) - if err != nil { - return msgs, err - } - msgs = append(msgs, &msg) - return msgs, nil + msgConfig.ReplyMarkup = NewReply(). + WithRemove(true). + ToApi() + ret = append(ret, &SendConfig{Message: &msgConfig}) + return ret, nil } - ch[0] = msgConfig + ret = append(ret, &SendConfig{Message: &msgConfig}) } // Screen text and reply keyboard. - txt = "" - if kbd != nil { - if kbd.Text != "" { - txt = kbd.Text - } else { - txt = ">" - } - msgConfig := tgbotapi.NewMessage(cid, txt) - msgConfig.ReplyMarkup = kbd.toTelegramReply() - ch[1] = msgConfig + 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 = tgbotapi.NewRemoveKeyboard(true) - ch[1] = msgConfig + msgConfig.ReplyMarkup = NewReply(). + WithRemove(true). + ToApi() + ret = append(ret, &SendConfig{Message: &msgConfig}) } - for _, m := range ch { - if m != nil { - msg, err := bot.Api.Send(m) - if err != nil { - return msgs, err - } - msgs = append(msgs, &msg) - } - } - - return msgs, nil + return ret, nil } diff --git a/tg/send.go b/tg/send.go index e6c84e3..f659ffa 100644 --- a/tg/send.go +++ b/tg/send.go @@ -1,11 +1,43 @@ package tg +import ( + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +type MessageId int64 +type Image any + // Implementing the interface lets the // value to be sent. type Sendable interface { - Send(SessionId, *Bot) (*Message, error) + SendConfig(SessionId, *Bot) (*SendConfig, error) } type Renderable interface { - Render(SessionId, *Bot) ([]*Message, error) + Render(SessionId, *Bot) ([]*SendConfig, 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 . + Message *tgbotapi.MessageConfig + + // The image to be sent. + Image *tgbotapi.PhotoConfig +} + +// Convert to the bot.Api.Send format. +func (config *SendConfig) ToApi() tgbotapi.Chattable { + if config.Message != nil { + return *config.Message + } + + if config.Image != nil { + return *config.Image + } + return nil +} +