diff --git a/.gitignore b/.gitignore index 40ff9a2..fc2a76b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ exe *.exe *.swp +tmp diff --git a/cmd/test/main.go b/cmd/test/main.go index d0ab2fd..0418ee7 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -1,111 +1,65 @@ package main import ( - "log" - "os" + "log" + "os" - //tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "boteval/src/behx" + "github.com/mojosa-software/got/src/tx" ) -var rootKbd = behx.NewKeyboard( - behx.NewButtonRow( - behx.NewButton( - "Increment", - behx.NewCustomAction(func(c *behx.Context){ - counter := c.V["counter"].(*int) - *counter++ - c.Sendf("%d", *counter) - }), - ), - behx.NewButton( - "Decrement", - behx.NewCustomAction(func(c *behx.Context){ - counter := c.V["counter"].(*int) - *counter-- - c.Sendf("%d", *counter) - }), - ), - ), - behx.NewButtonRow( - behx.NewButton("To second screen", behx.NewScreenChange("second")), - ), +var navKeyboard = tx.NewKeyboard("nav").Row( + tx.NewButton().WithText("Inc/Dec").ScreenChange("inc/dec"), +).Row( + tx.NewButton().WithText("Upper case").ScreenChange("upper-case"), + tx.NewButton().WithText("Lower case").ScreenChange("lower-case"), ) -var secondKbd = behx.NewKeyboard( - behx.NewButtonRow( - behx.NewButton( - "❤", - behx.NewScreenChange("start"), - ), - ), -) - -var inlineKbd = behx.NewKeyboard( - behx.NewButtonRow( - behx.NewButton( - "INLINE PRESS ME", - behx.NewCustomAction(func(c *behx.Context){ - log.Println("INLINE pressed the button!") - }), - ), - behx.NewButton("INLINE PRESS ME 2", behx.NewCustomAction(func(c *behx.Context){ - log.Println("INLINE pressed another button!") - })), - ), - behx.NewButtonRow( - behx.NewButton( - "INLINE PRESS ME 3", - behx.ScreenChange("second"), - ), - ), -) - - -var startScreen = behx.NewScreen( - "Hello, World!", - "inline", - "root", -) - -var secondScreen = behx.NewScreen( - "Second screen!", - "", - "second", -) - -var behaviour = behx.NewBehaviour( - behx.NewCustomAction(func(c *behx.Context){ - // This way we provide counter for EACH user. - c.V["counter"] = new(int) - - // Do NOT forget to change to some of the screens - // since they are the ones who provide behaviour - // definition. - c.ChangeScreen("start") +var incKeyboard = tx.NewKeyboard("inc/dec").Row( + tx.NewButton().WithText("+").ActionFunc(func(c *tx.Context) { + counter := c.V["counter"].(*int) + *counter++ + c.Sendf("%d", *counter) }), - behx.ScreenMap{ - "start": startScreen, - "second": secondScreen, - }, - behx.KeyboardMap{ - "root": rootKbd, - "inline": inlineKbd, - "second": secondKbd, - }, + tx.NewButton().WithText("-").ActionFunc(func(c *tx.Context) { + counter := c.V["counter"].(*int) + *counter-- + c.Sendf("%d", *counter) + }), +) + +var startScreen = tx.NewScreen("start"). + WithText("The bot started!"). + Keyboard("nav") + +var incScreen = tx.NewScreen("inc/dec"). + WithText("The screen shows how user separated data works"). + IKeyboard("inc/dec"). + Keyboard("nav") + +var beh = tx.NewBehaviour(). + OnStartFunc(func(c *tx.Context) { + // The function will be called every time + // the bot is started. + c.V["counter"] = new(int) + c.ChangeScreen("start") + }).WithKeyboards( + navKeyboard, + incKeyboard, +).WithScreens( + startScreen, + incScreen, ) func main() { token := os.Getenv("BOT_TOKEN") - - bot, err := behx.NewBot(token, behaviour, nil) - if err != nil { - log.Panic(err) - } - bot.Debug = true + bot, err := tx.NewBot(token, beh, nil) + if err != nil { + log.Panic(err) + } - log.Printf("Authorized on account %s", bot.Self.UserName) - bot.Run() + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + bot.Run() } - diff --git a/go.mod b/go.mod index 7fbaf75..6d26af9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module boteval +module github.com/mojosa-software/got go 1.20 diff --git a/src/behx/beh.go b/src/behx/beh.go deleted file mode 100644 index 53577f1..0000000 --- a/src/behx/beh.go +++ /dev/null @@ -1,39 +0,0 @@ -package behx - -// The package implements -// behaviour for the Telegram bots. - - -// The type describes behaviour for the bot. -type Behaviour struct { - Start Action - Screens ScreenMap - Keyboards KeyboardMap -} - -func NewBehaviour( - start Action, - screens ScreenMap, - keyboards KeyboardMap, -) *Behaviour { - return &Behaviour{ - start, screens, keyboards, - } -} - -// Check whether the screen exists in the behaviour. -func (beh *Behaviour) ScreenExists(id ScreenId) bool { - _, ok := beh.Screens[id] - return ok -} - -// Returns the screen by it's ID. -func (beh *Behaviour) GetScreen(id ScreenId) *Screen { - if !beh.ScreenExists(id) { - panic(ScreenNotExistErr) - } - - screen := beh.Screens[id] - return screen -} - diff --git a/src/behx/errors.go b/src/behx/errors.go deleted file mode 100644 index 47bb6b8..0000000 --- a/src/behx/errors.go +++ /dev/null @@ -1,13 +0,0 @@ -package behx - -import ( - "errors" -) - -var ( - ScreenNotExistErr = errors.New("screen does not exist") - SessionNotExistErr = errors.New("session does not exist") - KeyboardNotExistErr = errors.New("keyboard does not exist") -) - - diff --git a/src/behx/action.go b/src/tx/action.go similarity index 53% rename from src/behx/action.go rename to src/tx/action.go index 07d57ff..f0acfa9 100644 --- a/src/behx/action.go +++ b/src/tx/action.go @@ -1,4 +1,4 @@ -package behx +package tx // Implementing the intereface lets you // provide behaviour for the buttons etc. @@ -7,29 +7,21 @@ type Action interface { } // Customized action for the bot. -type CustomAction func(*Context) +type ActionFunc func(*Context) // The type implements changing screen to the underlying ScreenId type ScreenChange ScreenId -// Returns new ScreenChange. -func NewScreenChange(screen string) ScreenChange { - return ScreenChange(screen) -} - -// Returns new CustomAction. -func NewCustomAction(fn func(*Context)) CustomAction { - return CustomAction(fn) -} - func (sc ScreenChange) Act(c *Context) { + if !c.B.ScreenExist(ScreenId(sc)) { + panic(ScreenNotExistErr) + } err := c.ChangeScreen(ScreenId(sc)) if err != nil { panic(err) } } -func (ca CustomAction) Act(c *Context) { - ca(c) +func (af ActionFunc) Act(c *Context) { + af(c) } - diff --git a/src/tx/beh.go b/src/tx/beh.go new file mode 100644 index 0000000..c431156 --- /dev/null +++ b/src/tx/beh.go @@ -0,0 +1,86 @@ +package tx + +// The package implements +// behaviour for the Telegram bots. + +// The type describes behaviour for the bot. +type Behaviour struct { + Start Action + Screens ScreenMap + Keyboards KeyboardMap +} + +// Returns new empty behaviour. +func NewBehaviour() *Behaviour { + return &Behaviour{ + Screens: make(ScreenMap), + Keyboards: make(KeyboardMap), + } +} + +func (b *Behaviour) WithStart(a Action) *Behaviour { + b.Start = a + return b +} + +func (b *Behaviour) OnStartFunc( + fn ActionFunc, +) *Behaviour { + return b.WithStart(fn) +} + +func (b *Behaviour) OnStartChangeScreen( + id ScreenId, +) *Behaviour { + return b.WithStart(ScreenChange(id)) +} + +// The function sets keyboards. +func (b *Behaviour) WithKeyboards( + kbds ...*Keyboard, +) *Behaviour { + for _, kbd := range kbds { + if kbd.Id == "" { + panic("empty keyboard ID") + } + _, ok := b.Keyboards[kbd.Id] + if ok { + panic("duplicate keyboard IDs") + } + b.Keyboards[kbd.Id] = kbd + } + return b +} + +// The function sets screens. +func (b *Behaviour) WithScreens( + screens ...*Screen, +) *Behaviour { + for _, screen := range screens { + if screen.Id == "" { + panic("empty screen ID") + } + _, ok := b.Screens[screen.Id] + if ok { + panic("duplicate keyboard IDs") + } + b.Screens[screen.Id] = screen + } + return b +} + +// Check whether the screen exists in the behaviour. +func (beh *Behaviour) ScreenExist(id ScreenId) bool { + _, ok := beh.Screens[id] + return ok +} + +// Returns the screen by it's ID. +func (beh *Behaviour) GetScreen(id ScreenId) *Screen { + if !beh.ScreenExist(id) { + panic(ScreenNotExistErr) + } + + screen := beh.Screens[id] + return screen +} diff --git a/src/behx/bot.go b/src/tx/bot.go similarity index 69% rename from src/behx/bot.go rename to src/tx/bot.go index 0df63ee..2c264be 100644 --- a/src/behx/bot.go +++ b/src/tx/bot.go @@ -1,8 +1,8 @@ -package behx +package tx import ( - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" - //"log" + apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" + //"log" ) // The wrapper around Telegram API. @@ -14,45 +14,43 @@ type Bot struct { // Return the new bot for running the Behaviour. func NewBot(token string, beh *Behaviour, sessions SessionMap) (*Bot, error) { - bot, err := apix.NewBotAPI(token) - if err != nil { - return nil, err - } - - // Make new sessions if no current are provided. - if sessions == nil { - sessions = make(SessionMap) - } - - return &Bot{ - BotAPI: bot, + bot, err := apix.NewBotAPI(token) + if err != nil { + return nil, err + } + + // Make new sessions if no current are provided. + if sessions == nil { + sessions = make(SessionMap) + } + + return &Bot{ + BotAPI: bot, Behaviour: beh, - sessions: make(SessionMap), - - }, nil + sessions: make(SessionMap), + }, nil } // Run the bot with the Behaviour. func (bot *Bot) Run() error { bot.Debug = true - + uc := apix.NewUpdate(0) uc.Timeout = 60 - + updates := bot.GetUpdatesChan(uc) - - chans := make(map[SessionId] chan *Update) + + chans := make(map[SessionId]chan *Update) for u := range updates { var sid SessionId - if u.Message != nil { + if u.Message != nil { // Create new session if the one does not exist // for this user. sid = SessionId(u.Message.Chat.ID) - if _, ok := bot.sessions[sid] ; !ok { + if _, ok := bot.sessions[sid]; !ok { bot.sessions.Add(sid) - } - - + } + // The "start" command resets the bot // by executing the Start Action. if u.Message.IsCommand() { @@ -61,10 +59,10 @@ func (bot *Bot) Run() error { // Getting current session and context. session := bot.sessions[sid] ctx := &Context{ - B: bot, + B: bot, Session: session, } - + chn := make(chan *Update) chans[sid] = chn // Starting the goroutine for the user. @@ -80,7 +78,6 @@ func (bot *Bot) Run() error { chn <- &u } } - + return nil } - diff --git a/src/behx/button.go b/src/tx/button.go similarity index 64% rename from src/behx/button.go rename to src/tx/button.go index ae96e05..e939b8e 100644 --- a/src/behx/button.go +++ b/src/tx/button.go @@ -1,42 +1,62 @@ -package behx +package tx import ( - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" + apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) // The type wraps Telegram API's button to provide Action functionality. type Button struct { - Text string - Data string - Url string + Text string + Data string + Url string Action Action } -type ButtonMap map[string] *Button +type ButtonMap map[string]*Button // Represents the reply button row. type ButtonRow []*Button // Returns new button with specified text and action. -func NewButton(text string, action Action) *Button { - return &Button{ - Text: text, - Action: action, - } +func NewButton() *Button { + return &Button{} +} + +func (btn *Button) WithText(text string) *Button { + btn.Text = text + return btn +} + +func (btn *Button) WithUrl(url string) *Button { + btn.Url = url + return btn +} + +func (btn *Button) WithAction(a Action) *Button { + btn.Action = a + return btn +} + +func (btn *Button) ActionFunc(fn ActionFunc) *Button { + return btn.WithAction(fn) +} + +func (btn *Button) ScreenChange(sc ScreenChange) *Button { + return btn.WithAction(sc) } func NewButtonData(text string, data string, action Action) *Button { return &Button{ - Text: text, - Data: data, + Text: text, + Data: data, Action: action, } } func NewButtonUrl(text string, url string, action Action) *Button { return &Button{ - Text: text, - Url: url, + Text: text, + Url: url, Action: action, } } @@ -49,11 +69,11 @@ func (btn *Button) ToTelegramInline() apix.InlineKeyboardButton { if btn.Data != "" { return apix.NewInlineKeyboardButtonData(btn.Text, btn.Data) } - + if btn.Url != "" { return apix.NewInlineKeyboardButtonURL(btn.Text, btn.Url) } - + // If no match then return the data one with data the same as the text. return apix.NewInlineKeyboardButtonData(btn.Text, btn.Text) } @@ -63,7 +83,7 @@ func (btn *Button) Key() string { if btn.Data != "" { return btn.Data } - + // If no match then return the data one with data the same as the text. return btn.Text } @@ -71,4 +91,3 @@ func (btn *Button) Key() string { func NewButtonRow(btns ...*Button) ButtonRow { return btns } - diff --git a/src/behx/context.go b/src/tx/context.go similarity index 95% rename from src/behx/context.go rename to src/tx/context.go index 510c410..238f8fb 100644 --- a/src/behx/context.go +++ b/src/tx/context.go @@ -1,8 +1,9 @@ -package behx +package tx import ( - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" "fmt" + + apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) // The type represents way to interact with user in @@ -19,24 +20,24 @@ func (ctx *Context) handleUpdateChan(updates chan *Update) { bot.Start.Act(ctx) for u := range updates { screen := bot.Screens[session.CurrentScreenId] - + if u.Message != nil { - + kbd := bot.Keyboards[screen.KeyboardId] btns := kbd.buttonMap() text := u.Message.Text btn, ok := btns[text] - + // Skipping wrong text messages. if !ok { continue } - + btn.Action.Act(ctx) } else if u.CallbackQuery != nil { cb := apix.NewCallback(u.CallbackQuery.ID, u.CallbackQuery.Data) data := u.CallbackQuery.Data - + _, err := bot.Request(cb) if err != nil { panic(err) @@ -55,17 +56,17 @@ func (c *Context) ChangeScreen(screenId ScreenId) error { if c.CurrentScreenId == screenId { return nil } - - if !c.B.ScreenExists(screenId) { + + if !c.B.ScreenExist(screenId) { return ScreenNotExistErr } - + screen := c.B.Screens[screenId] screen.Render(c) - + c.Session.ChangeScreen(screenId) c.KeyboardId = screen.KeyboardId - + return nil } @@ -79,4 +80,3 @@ func (c *Context) Send(text string) error { func (c *Context) Sendf(format string, v ...any) error { return c.Send(fmt.Sprintf(format, v...)) } - diff --git a/src/tx/errors.go b/src/tx/errors.go new file mode 100644 index 0000000..60ee41a --- /dev/null +++ b/src/tx/errors.go @@ -0,0 +1,11 @@ +package tx + +import ( + "errors" +) + +var ( + ScreenNotExistErr = errors.New("screen does not exist") + SessionNotExistErr = errors.New("session does not exist") + KeyboardNotExistErr = errors.New("keyboard does not exist") +) diff --git a/src/behx/keyboard.go b/src/tx/keyboard.go similarity index 75% rename from src/behx/keyboard.go rename to src/tx/keyboard.go index f81cb66..0805164 100644 --- a/src/behx/keyboard.go +++ b/src/tx/keyboard.go @@ -1,8 +1,9 @@ -package behx +package tx import ( - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" + apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) + /* var otherKeyboard = tgbotapi.NewReplyKeyboard( tgbotapi.NewKeyboardButtonRow( @@ -22,20 +23,27 @@ type KeyboardId string // The type represents reply keyboard which // is supposed to be showed on a Screen. type Keyboard struct { + Id KeyboardId Rows []ButtonRow } -type KeyboardMap map[KeyboardId] *Keyboard +type KeyboardMap map[KeyboardId]*Keyboard // Return the new reply keyboard with rows as specified. -func NewKeyboard(rows ...ButtonRow) *Keyboard { +func NewKeyboard(id KeyboardId) *Keyboard { return &Keyboard{ - Rows: rows, + Id: id, } } +// Adds a new button row to the current keyboard. +func (kbd *Keyboard) Row(btns ...*Button) *Keyboard { + kbd.Rows = append(kbd.Rows, btns) + return kbd +} + // Convert the Keyboard to the Telegram API type. -func (kbd *Keyboard) ToTelegram() apix.ReplyKeyboardMarkup { +func (kbd *Keyboard) toTelegram() apix.ReplyKeyboardMarkup { rows := [][]apix.KeyboardButton{} for _, row := range kbd.Rows { buttons := []apix.KeyboardButton{} @@ -43,12 +51,12 @@ func (kbd *Keyboard) ToTelegram() apix.ReplyKeyboardMarkup { buttons = append(buttons, button.ToTelegram()) } rows = append(rows, buttons) - } - + } + return apix.NewReplyKeyboard(rows...) } -func (kbd *Keyboard) ToTelegramInline() apix.InlineKeyboardMarkup { +func (kbd *Keyboard) toTelegramInline() apix.InlineKeyboardMarkup { rows := [][]apix.InlineKeyboardButton{} for _, row := range kbd.Rows { buttons := []apix.InlineKeyboardButton{} @@ -56,8 +64,8 @@ func (kbd *Keyboard) ToTelegramInline() apix.InlineKeyboardMarkup { buttons = append(buttons, button.ToTelegramInline()) } rows = append(rows, buttons) - } - + } + return apix.NewInlineKeyboardMarkup(rows...) } @@ -69,7 +77,6 @@ func (kbd *Keyboard) buttonMap() ButtonMap { ret[vj.Key()] = vj } } - + return ret } - diff --git a/src/behx/main.go b/src/tx/main.go similarity index 86% rename from src/behx/main.go rename to src/tx/main.go index 2a51af6..8c5c56b 100644 --- a/src/behx/main.go +++ b/src/tx/main.go @@ -1,5 +1,4 @@ -package behx +package tx // The package implements behaviourial // definition for the Telegram bots through the API. - diff --git a/src/behx/screen.go b/src/tx/screen.go similarity index 72% rename from src/behx/screen.go rename to src/tx/screen.go index cb130f5..243a578 100644 --- a/src/behx/screen.go +++ b/src/tx/screen.go @@ -1,4 +1,4 @@ -package behx +package tx import ( apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" @@ -14,28 +14,44 @@ type ScreenText string // Screen statement of the bot. // Mostly what buttons to show. type Screen struct { + Id ScreenId + // Text to be sent to the user when changing to the screen. Text ScreenText - + // The keyboard to be sent in the message part. InlineKeyboardId KeyboardId - + // Keyboard to be displayed on the screen. KeyboardId KeyboardId } // Map structure for the screens. -type ScreenMap map[ScreenId] *Screen +type ScreenMap map[ScreenId]*Screen // Returns the new screen with specified Text and Keyboard. -func NewScreen(text ScreenText, ikbd KeyboardId, kbd KeyboardId) *Screen { - return &Screen { - Text: text, - InlineKeyboardId: ikbd, - KeyboardId: kbd, +func NewScreen(id ScreenId) *Screen { + return &Screen{ + Id: id, } } +// Returns the screen with specified text printing on appearing. +func (s *Screen) WithText(text ScreenText) *Screen { + s.Text = text + return s +} + +func (s *Screen) IKeyboard(kbdId KeyboardId) *Screen { + s.InlineKeyboardId = kbdId + return s +} + +func (s *Screen) Keyboard(kbdId KeyboardId) *Screen { + s.KeyboardId = kbdId + return s +} + // Rendering the screen text to string to be sent or printed. func (st ScreenText) String() string { return string(st) @@ -44,44 +60,43 @@ func (st ScreenText) String() string { // Renders output of the screen only to the side of the user. func (s *Screen) Render(c *Context) error { id := c.Id.ToTelegram() - + msg := apix.NewMessage(id, s.Text.String()) - + if s.InlineKeyboardId != "" { kbd, ok := c.B.Keyboards[s.InlineKeyboardId] if !ok { return KeyboardNotExistErr } - msg.ReplyMarkup = kbd.ToTelegramInline() + msg.ReplyMarkup = kbd.toTelegramInline() } - + _, err := c.B.Send(msg) if err != nil { return err } - + msg = apix.NewMessage(id, ">") // Checking if we need to resend the keyboard. if s.KeyboardId != c.KeyboardId { // Remove keyboard by default. var tkbd any tkbd = apix.NewRemoveKeyboard(true) - + // Replace keyboard with the new one. if s.KeyboardId != "" { kbd, ok := c.B.Keyboards[s.KeyboardId] if !ok { return KeyboardNotExistErr } - tkbd = kbd.ToTelegram() + tkbd = kbd.toTelegram() } - + msg.ReplyMarkup = tkbd - if _, err := c.B.Send(msg) ; err != nil { + if _, err := c.B.Send(msg); err != nil { return err } - } - + } + return nil } - diff --git a/src/behx/session.go b/src/tx/session.go similarity index 89% rename from src/behx/session.go rename to src/tx/session.go index f012494..79b5a7d 100644 --- a/src/behx/session.go +++ b/src/tx/session.go @@ -1,4 +1,4 @@ -package behx +package tx import ( apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" @@ -21,20 +21,20 @@ type Session struct { PreviousScreenId ScreenId // The currently showed on display keyboard. KeyboardId KeyboardId - + // Custom data for each user. - V map[string] any + V map[string]any } // The type represents map of sessions using // as key. -type SessionMap map[SessionId] *Session +type SessionMap map[SessionId]*Session -// Return new empty session with +// Return new empty session with func NewSession(id SessionId) *Session { return &Session{ Id: id, - V: make(map[string] any), + V: make(map[string]any), } }