Implemented basic keyboard and screen rendering.

This commit is contained in:
Andrey Parhomenko 2023-07-12 00:33:51 +03:00
parent 564f1e8dc4
commit c537ccdec1
12 changed files with 105 additions and 102 deletions

5
go.mod
View file

@ -2,4 +2,7 @@ module boteval
go 1.20 go 1.20
require github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect require (
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 // indirect
github.com/mojosa-software/godat v0.0.0-20230711170316-a335bad31575 // indirect
)

2
go.sum
View file

@ -1,2 +1,4 @@
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
github.com/mojosa-software/godat v0.0.0-20230711170316-a335bad31575 h1:wtaYzLbEND7JSY7L+huFU4IDT4nbuDolKRfnQ1d6KTc=
github.com/mojosa-software/godat v0.0.0-20230711170316-a335bad31575/go.mod h1:E6ohOj8PpUJBQOSRdrLygjoO+Te6yfeox3ZtaItsHUg=

View file

@ -3,11 +3,11 @@ package behx
// Implementing the intereface lets you // Implementing the intereface lets you
// provide behaviour for the buttons etc. // provide behaviour for the buttons etc.
type Action interface { type Action interface {
Act(*Bot) Act(*Context)
} }
// Customized action for the bot. // Customized action for the bot.
type CustomAction func(*Bot) type CustomAction func(*Context)
// The type implements changing screen to the underlying ScreenId // The type implements changing screen to the underlying ScreenId
type ScreenChange ScreenId type ScreenChange ScreenId
@ -18,7 +18,7 @@ func NewScreenChange(screen string) ScreenChange {
} }
// Returns new CustomAction. // Returns new CustomAction.
func NewCustomAction(fn func(*Bot)) CustomAction { func NewCustomAction(fn func(*Context)) CustomAction {
return CustomAction(fn) return CustomAction(fn)
} }
@ -27,7 +27,7 @@ func (sc ScreenChange) Act(c *Context) {
} }
func (ca CustomAction) Act(c *Context) { func (ca CustomAction) Act(c *Context) {
ca(bot) ca(c)
} }

View file

@ -13,7 +13,7 @@ type Behaviour struct {
// Check whether the screen exists in the behaviour. // Check whether the screen exists in the behaviour.
func (beh *Behaviour) ScreenExists(id ScreenId) bool { func (beh *Behaviour) ScreenExists(id ScreenId) bool {
_, ok := bot.behaviour.Screens[id] _, ok := beh.Screens[id]
return ok return ok
} }

View file

@ -13,7 +13,7 @@ type Bot struct {
// Return the new bot for running the Behaviour. // Return the new bot for running the Behaviour.
func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) { func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) {
bot, err := tgbotapi.NewBotAPI(token) bot, err := apix.NewBotAPI(token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -26,6 +26,7 @@ func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) {
return &Bot{ return &Bot{
BotAPI: bot, BotAPI: bot,
Behaviour: beh, Behaviour: beh,
sessions: make(SessionMap),
}, nil }, nil
} }
@ -34,34 +35,38 @@ func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) {
func (bot *Bot) Run() error { func (bot *Bot) Run() error {
bot.Debug = true bot.Debug = true
uc := tgbotapi.NewUpdate(0) uc := apix.NewUpdate(0)
uc.Timeout = 60 uc.Timeout = 60
updates := bot.GetUpdatesChan(uc) updates := bot.GetUpdatesChan(uc)
for u := range updates { for u := range updates {
// Create new session if the one does not exist // Create new session if the one does not exist
// for this user. // for this user.
sid := SessionId(u.Message.Chat.ID) sid := SessionId(u.Message.Chat.ID)
if !bot.sessions.Exist(sid) { if _, ok := bot.sessions[sid] ; !ok {
bot.sessions.Add(sid) bot.sessions.Add(sid)
} }
session := bot.sessions.Get(sid) session := bot.sessions[sid]
ctx := &beh.Context{ ctx := &Context{
B: bot, B: bot,
S: session, S: session,
} }
// The "start" command resets the bot // The "start" command resets the bot
// by executing the Start Action. // by executing the Start Action.
if u.MessageIsCommand() { if u.Message.IsCommand() {
cmd := u.Message.Command() cmd := u.Message.Command()
if cmd == "start" { if cmd == "start" {
bot.Start.Act(ctx) bot.Start.Act(ctx)
} }
continue continue
} }
screen := bot.Screens[session.CurrentScreenId]
screen.Render(ctx)
} }
return nil return nil

View file

@ -7,7 +7,7 @@ import (
// The type wraps Telegram API's button to provide Action functionality. // The type wraps Telegram API's button to provide Action functionality.
type Button struct { type Button struct {
apix.KeyboardButton apix.KeyboardButton
Action Acter Action Action
} }
type ButtonMap map[string] *Button type ButtonMap map[string] *Button
@ -18,7 +18,7 @@ type ButtonRow []*Button
// Returns new button with specified text and action. // Returns new button with specified text and action.
func NewButton(text string, action Action) *Button { func NewButton(text string, action Action) *Button {
return &Button{ return &Button{
KeyboardButton: apix.NewKeyboardButton(text) KeyboardButton: apix.NewKeyboardButton(text),
Action: action, Action: action,
} }
} }

View file

@ -1,20 +1,32 @@
package behx package behx
import (
apix "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
type Update = apix.Update
// The type represents way to interact with user in // The type represents way to interact with user in
// handling functions. Is provided to Act() function always. // handling functions. Is provided to Act() function always.
type Context struct { type Context struct {
S *Session S *Session
B *Bot B *Bot
U *Update
} }
func (c *Context) ChangeScreen(screen ScreenId) error { // Changes screen of user to the Id one.
if !bot.ScreenExists(screenId) { func (c *Context) ChangeScreen(screenId ScreenId) error {
if !c.B.ScreenExists(screenId) {
return ScreenNotExistErr return ScreenNotExistErr
} }
c.S.PreviousScreenId = c.S.CurrentScreenId
c.S.CurrentScreenId = screenId
return nil return nil
} }
// Sends to the user specified text.
func (c *Context) Send(text string) error { func (c *Context) Send(text string) error {
return nil return nil

View file

@ -20,29 +20,39 @@ var otherKeyboard = tgbotapi.NewReplyKeyboard(
// The type represents reply keyboard which // The type represents reply keyboard which
// is supposed to be showed on a Screen. // is supposed to be showed on a Screen.
type Keyboard struct { type Keyboard struct {
rows []ButtonRow Rows []ButtonRow
} }
// Return the new reply keyboard with rows as specified. // Return the new reply keyboard with rows as specified.
func NewKeyboard(rows ...ButtonRow) *Keyboard { func NewKeyboard(rows ...ButtonRow) *Keyboard {
return &Keyboard{ return &Keyboard{
rows: rows, Rows: rows,
} }
} }
// Convert the Keyboard to the Telegram API type. // Convert the Keyboard to the Telegram API type.
func (kbd *Keyboard) ToTelegram() ReplyKeyboardMarkup { func (kbd *Keyboard) ToTelegram() apix.ReplyKeyboardMarkup {
rows := [][]apix.KeyboardButton{} rows := [][]apix.KeyboardButton{}
for _, row := range kbd.rows { for _, row := range kbd.Rows {
buttons := []apix.KeyboardButton{} buttons := []apix.KeyboardButton{}
for _, button := range row { for _, button := range row {
buttons = append(buttons, apix.NewKeyboardButton(button.text)) buttons = append(buttons, apix.NewKeyboardButton(button.Text))
} }
rows = append(rows, buttons) rows = append(rows, buttons)
} }
return apix.NewReplyKeyboard(rows) return apix.NewReplyKeyboard(rows...)
} }
func (kbd *Keyboard) ButtonMap() ButtonMap // Returns the map of buttons. Used to define the Action.
func (kbd *Keyboard) buttonMap() ButtonMap {
ret := make(ButtonMap)
for _, vi := range kbd.Rows {
for _, vj := range vi {
ret[vj.Text] = vj
}
}
return ret
}

View file

@ -1,20 +0,0 @@
package behx
type Map[K comparable, V any] map[K] V
func (m Map[K, V]) Exist(k K) bool {
_, ok := sm[sid]
return ok
}
func (m Map[K, V]) Get(k K, v V) V {
ret := m[sid]
return ret
}
func (sm Map[K, V]) Set(v V) {
sm[sid] = &Session{
Id: sid,
}
}

View file

@ -1,5 +1,9 @@
package behx package behx
import (
apix "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// Unique identifier for the screen. // Unique identifier for the screen.
type ScreenId string type ScreenId string
@ -14,11 +18,12 @@ type Screen struct {
Text ScreenText Text ScreenText
// Keyboard to be displayed on the screen. // Keyboard to be displayed on the screen.
Keyboard *Keyboard Keyboard *Keyboard
// The keyboard to be sent in the message part.
InlineKeyboard *Keyboard
} }
// Map structure for the screens. // Map structure for the screens.
type ScreenMap Map[ScreenId, *Screen] type ScreenMap map[ScreenId] *Screen
map[ScreenId] *Screen
// Returns the new screen with specified Text and Keyboard. // Returns the new screen with specified Text and Keyboard.
func NewScreen(text ScreenText, kbd *Keyboard) *Screen { func NewScreen(text ScreenText, kbd *Keyboard) *Screen {
@ -28,4 +33,31 @@ func NewScreen(text ScreenText, kbd *Keyboard) *Screen {
} }
} }
// Rendering the screen text to string to be sent or printed.
func (st ScreenText) String() string {
return string(st)
}
// Renders output of the screen to the side of the user.
func (s *Screen) Render(c *Context) {
id := c.S.Id.ToTelegram()
msg := apix.NewMessage(id, s.Text.String())
// First sending the inline keyboard.
if s.InlineKeyboard != nil {
msg.ReplyMarkup = s.InlineKeyboard.ToTelegram()
if _, err := c.B.Send(msg) ; err != nil {
panic(err)
}
}
// Then sending the screen one.
if s.Keyboard != nil {
msg = apix.NewMessage(id, "check")
msg.ReplyMarkup = s.Keyboard.ToTelegram()
if _, err := c.B.Send(msg) ; err != nil {
panic(err)
}
}
}

View file

@ -16,4 +16,13 @@ type Session struct {
// as key. // as key.
type SessionMap map[SessionId] *Session type SessionMap map[SessionId] *Session
func (si SessionId) ToTelegram() int64 {
return int64(si)
}
func (sm SessionMap) Add(sid SessionId) {
sm[sid] = &Session{
Id: sid,
}
}

View file

@ -4,11 +4,12 @@ import (
"log" "log"
"os" "os"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" //tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"boteval/src/cmd/behx" "boteval/src/behx"
) )
var startScreen = behx.NewScreen( var startScreen = behx.NewScreen(
"Hello, World!",
behx.NewKeyboard( behx.NewKeyboard(
behx.NewButtonRow( behx.NewButtonRow(
behx.NewButton( behx.NewButton(
@ -29,43 +30,17 @@ var startScreen = behx.NewScreen(
), ),
) )
var behaviour = behx.Behaviour{ var behaviour = &behx.Behaviour{
StartAction: behx.NewScreenChange("start"), Start: behx.NewScreenChange("start"),
Screens: behx.ScreenMap{ Screens: behx.ScreenMap{
"start": startScreen, "start": startScreen,
}, },
} }
var numericKeyboard = tgbotapi.NewReplyKeyboard(
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("1"),
tgbotapi.NewKeyboardButton("2"),
tgbotapi.NewKeyboardButton("3"),
),
tgbotapi.NewKeyboardButtonRow(
tgbotapi.NewKeyboardButton("4"),
tgbotapi.NewKeyboardButton("5"),
tgbotapi.NewKeyboardButton("6"),
),
)
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"),
),
)
func main() { func main() {
token := os.Getenv("BOT_TOKEN") token := os.Getenv("BOT_TOKEN")
bot, err := tgbotapi.NewBotAPI(token) bot, err := behx.NewBot(token, behaviour, nil)
if err != nil { if err != nil {
log.Panic(err) log.Panic(err)
} }
@ -73,31 +48,6 @@ func main() {
bot.Debug = true bot.Debug = true
log.Printf("Authorized on account %s", bot.Self.UserName) log.Printf("Authorized on account %s", bot.Self.UserName)
bot.Run()
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil { // ignore non-Message updates
continue
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
switch update.Message.Text {
case "open":
msg.ReplyMarkup = numericKeyboard
case "letters" :
msg.ReplyMarkup = otherKeyboard
case "close":
msg.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true)
}
if _, err := bot.Send(msg); err != nil {
log.Panic(err)
}
}
} }