Implemented synchronous reading from the client letting us read messages.

This commit is contained in:
Andrey Parhomenko 2023-08-11 10:41:17 +03:00
parent fbfa0ec388
commit cd965fd811
6 changed files with 165 additions and 47 deletions

View file

@ -3,51 +3,76 @@ package main
import (
"log"
"os"
"strings"
"github.com/mojosa-software/got/src/tx"
)
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 incKeyboard = tx.NewKeyboard("inc/dec").Row(
tx.NewButton().WithText("+").ActionFunc(func(c *tx.Context) {
counter := c.V["counter"].(*int)
*counter++
c.Sendf("%d", *counter)
}),
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 startScreenButton = tx.NewButton().
WithText("🏠 To the start screen").
ScreenChange("start")
var beh = tx.NewBehaviour().
// The function will be called every time
// the bot is started.
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,
// Increment/decrement keyboard.
tx.NewKeyboard("inc/dec").Row(
tx.NewButton().WithText("+").ActionFunc(func(c *tx.Context) {
counter := c.V["counter"].(*int)
*counter++
c.Sendf("%d", *counter)
}),
tx.NewButton().WithText("-").ActionFunc(func(c *tx.Context) {
counter := c.V["counter"].(*int)
*counter--
c.Sendf("%d", *counter)
}),
).Row(
startScreenButton,
),
// The navigational keyboard.
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"),
),
// The keyboard to return to the start screen.
tx.NewKeyboard("nav-start").Row(
startScreenButton,
),
).WithScreens(
startScreen,
incScreen,
tx.NewScreen("start").
WithText("The bot started!").
Keyboard("nav"),
tx.NewScreen("inc/dec").
WithText(
"The screen shows how"+
"user separated data works"+
"by saving the counter for each of them",
).
Keyboard("inc/dec").
// The function will be called when reaching the screen.
ActionFunc(func(c *tx.Context) {
counter := c.V["counter"].(*int)
c.Sendf("Current counter value equals %d", *counter)
}),
tx.NewScreen("upper-case").
WithText("Type text and the bot will send you the upper case version to you").
Keyboard("nav-start").
ActionFunc(func(c *tx.Context) {
for {
s, err := c.ReadTextMessage()
if err == tx.NotAvailableErr {
break
}
c.Sendf("%s", strings.ToUpper(s))
}
}),
)
func main() {

View file

@ -61,6 +61,7 @@ func (bot *Bot) Run() error {
ctx := &Context{
B: bot,
Session: session,
updates: make(chan *Update),
}
chn := make(chan *Update)

View file

@ -10,17 +10,19 @@ import (
// handling functions. Is provided to Act() function always.
type Context struct {
*Session
B *Bot
B *Bot
updates chan *Update
available bool
}
// Goroutie function to handle each user.
func (ctx *Context) handleUpdateChan(updates chan *Update) {
bot := ctx.B
session := ctx.Session
bot.Start.Act(ctx)
func (c *Context) handleUpdateChan(updates chan *Update) {
bot := c.B
session := c.Session
bot.Start.Act(c)
for u := range updates {
screen := bot.Screens[session.CurrentScreenId]
// The part is added to implement custom update handling.
if u.Message != nil {
kbd := bot.Keyboards[screen.KeyboardId]
@ -28,12 +30,26 @@ func (ctx *Context) handleUpdateChan(updates chan *Update) {
text := u.Message.Text
btn, ok := btns[text]
// Skipping wrong text messages.
if !ok {
/*if ok {
c.available = false
btn.Action.Act(c)
c.available = true
continue
}*/
// Sending wrong messages to
// the currently reading goroutine.
if !ok && c.ReadingUpdate {
c.updates <- u
continue
}
btn.Action.Act(ctx)
if !ok {
}
if ok {
c.run(btn.Action)
}
} else if u.CallbackQuery != nil {
cb := apix.NewCallback(u.CallbackQuery.ID, u.CallbackQuery.Data)
data := u.CallbackQuery.Data
@ -45,11 +61,20 @@ func (ctx *Context) handleUpdateChan(updates chan *Update) {
kbd := bot.Keyboards[screen.InlineKeyboardId]
btns := kbd.buttonMap()
btn := btns[data]
btn.Action.Act(ctx)
btn.Action.Act(c)
}
}
}
func (c *Context) run(a Action) {
c.available = true
go a.Act(c)
}
func (c *Context) Available() bool {
return c.available
}
// Changes screen of user to the Id one.
func (c *Context) ChangeScreen(screenId ScreenId) error {
// Return if it will not change anything.
@ -67,16 +92,55 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
c.Session.ChangeScreen(screenId)
c.KeyboardId = screen.KeyboardId
c.available = false
if screen.Action != nil {
c.run(screen.Action)
}
return nil
}
// Returns the next update ignoring current screen.
func (c *Context) ReadUpdate() (*Update, error) {
var (
u *Update
)
c.ReadingUpdate = true
for {
select {
case u = <-c.updates:
c.ReadingUpdate = false
return u, nil
default:
if !c.available {
return nil, NotAvailableErr
}
}
}
}
// Returns the next text message that the user sends.
func (c *Context) ReadTextMessage() (string, error) {
u, err := c.ReadUpdate()
if err != nil {
return "", err
}
if u.Message == nil {
return "", WrongUpdateType{}
}
return u.Message.Text, nil
}
// Sends to the user specified text.
func (c *Context) Send(text string) error {
msg := apix.NewMessage(c.Id.ToTelegram(), text)
func (c *Context) Send(v ...any) error {
msg := apix.NewMessage(c.Id.ToTelegram(), fmt.Sprint(v...))
_, err := c.B.Send(msg)
return err
}
// Sends the formatted with fmt.Sprintf message to the user.
func (c *Context) Sendf(format string, v ...any) error {
return c.Send(fmt.Sprintf(format, v...))
}

View file

@ -2,10 +2,23 @@ package tx
import (
"errors"
"fmt"
)
type WrongUpdateType struct {
Type string
}
var (
ScreenNotExistErr = errors.New("screen does not exist")
SessionNotExistErr = errors.New("session does not exist")
KeyboardNotExistErr = errors.New("keyboard does not exist")
NotAvailableErr = errors.New("the context is not available")
)
func (wut WrongUpdateType) Error() string {
if wut.Type == "" {
return "wrong update type"
}
return fmt.Sprintf("wrong update type '%s'", wut.Type)
}

View file

@ -24,6 +24,9 @@ type Screen struct {
// Keyboard to be displayed on the screen.
KeyboardId KeyboardId
// Action called on the reaching the screen.
Action Action
}
// Map structure for the screens.
@ -52,6 +55,15 @@ func (s *Screen) Keyboard(kbdId KeyboardId) *Screen {
return s
}
func (s *Screen) WithAction(a Action) *Screen {
s.Action = a
return s
}
func (s *Screen) ActionFunc(a ActionFunc) *Screen {
return s.WithAction(a)
}
// Rendering the screen text to string to be sent or printed.
func (st ScreenText) String() string {
return string(st)

View file

@ -19,9 +19,12 @@ type Session struct {
CurrentScreenId ScreenId
// ID of the previous screen.
PreviousScreenId ScreenId
// The currently showed on display keyboard.
// The currently showed on display keyboard inside Action.
KeyboardId KeyboardId
// Is true if currently reading the Update.
ReadingUpdate bool
// Custom data for each user.
V map[string]any
}