Great refactoring. Should put more of the handling into the Actions.

This commit is contained in:
Andrey Parhomenko 2023-09-08 17:37:32 +03:00
parent b2748a8cee
commit 5b00189ea2
9 changed files with 227 additions and 166 deletions

View file

@ -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

View file

@ -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),
}
}

View file

@ -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(

View file

@ -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
}

View file

@ -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 {

51
tg/message.go Normal file
View file

@ -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
}

View file

@ -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)
}

View file

@ -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 {
} 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
msgConfig.ReplyMarkup = NewReply().
WithRemove(true).
ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
return ret, nil
}
msgs = append(msgs, &msg)
return msgs, 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
}

View file

@ -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
}