Great refactoring and rethinking of the Context.

This commit is contained in:
Andrey Parhomenko 2023-09-20 22:48:35 +03:00
parent d99ea68198
commit 1fc2acbbd6
7 changed files with 157 additions and 90 deletions

View file

@ -26,11 +26,14 @@ func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget {
return ret return ret
} }
func (w *MutateMessageWidget) Serve(c *tg.Context, updates *tg.UpdateChan) { func (w *MutateMessageWidget) Serve(c *tg.Context) {
for _, arg := range c.Args { args, ok := c.Arg.([]any)
if ok {
for _, arg := range args {
c.Sendf("%v", arg) c.Sendf("%v", arg)
} }
for u := range updates.Chan() { }
for u := range c.Input() {
text := u.Message.Text text := u.Message.Text
c.Sendf("%s", w.Mutate(text)) c.Sendf("%s", w.Mutate(text))
} }
@ -49,7 +52,7 @@ func ExtractSessionData(c *tg.Context) *SessionData {
var ( var (
startScreenButton = tg.NewButton("🏠 To the start screen"). startScreenButton = tg.NewButton("🏠 To the start screen").
ScreenChange("start") ScreenChange("/start")
incDecKeyboard = tg.NewKeyboard().Row( incDecKeyboard = tg.NewKeyboard().Row(
tg.NewButton("+").ActionFunc(func(c *tg.Context) { tg.NewButton("+").ActionFunc(func(c *tg.Context) {
@ -67,14 +70,14 @@ var (
) )
navKeyboard = tg.NewKeyboard().Row( navKeyboard = tg.NewKeyboard().Row(
tg.NewButton("Inc/Dec").ScreenChange("start/inc-dec"), tg.NewButton("Inc/Dec").ScreenChange("/start/inc-dec"),
).Row( ).Row(
tg.NewButton("Upper case").ActionFunc(func(c *tg.Context){ tg.NewButton("Upper case").ActionFunc(func(c *tg.Context){
c.ChangeScreen("start/upper-case", "this shit", "works") c.ChangeScreen("/start/upper-case", "this shit", "works")
}), }),
tg.NewButton("Lower case").ScreenChange("start/lower-case"), tg.NewButton("Lower case").ScreenChange("/start/lower-case"),
).Row( ).Row(
tg.NewButton("Send location").ScreenChange("start/send-location"), tg.NewButton("Send location").ScreenChange("/start/send-location"),
).Reply().WithOneTime(true) ).Reply().WithOneTime(true)
sendLocationKeyboard = tg.NewKeyboard().Row( sendLocationKeyboard = tg.NewKeyboard().Row(
@ -107,7 +110,7 @@ var beh = tg.NewBehaviour().
// The session initialization. // The session initialization.
c.Session.Data = &SessionData{} c.Session.Data = &SessionData{}
}).WithScreens( }).WithScreens(
tg.NewScreen("start", tg.NewPage( tg.NewScreen("/start", tg.NewPage(
"", "",
).WithInline( ).WithInline(
tg.NewKeyboard().Row( tg.NewKeyboard().Row(
@ -118,7 +121,7 @@ var beh = tg.NewBehaviour().
navKeyboard.Widget("Choose what you are interested in"), navKeyboard.Widget("Choose what you are interested in"),
), ),
), ),
tg.NewScreen("start/inc-dec", tg.NewPage( tg.NewScreen("/start/inc-dec", tg.NewPage(
"The screen shows how "+ "The screen shows how "+
"user separated data works "+ "user separated data works "+
"by saving the counter for each of users "+ "by saving the counter for each of users "+
@ -132,7 +135,7 @@ var beh = tg.NewBehaviour().
}), }),
), ),
tg.NewScreen("start/upper-case", tg.NewPage( tg.NewScreen("/start/upper-case", tg.NewPage(
"Type text and the bot will send you the upper case version to you", "Type text and the bot will send you the upper case version to you",
).WithReply( ).WithReply(
navToStartKeyboard.Widget(""), navToStartKeyboard.Widget(""),
@ -141,7 +144,7 @@ var beh = tg.NewBehaviour().
), ),
), ),
tg.NewScreen("start/lower-case", tg.NewPage( tg.NewScreen("/start/lower-case", tg.NewPage(
"Type text and the bot will send you the lower case version", "Type text and the bot will send you the lower case version",
).WithReply( ).WithReply(
navToStartKeyboard.Widget(""), navToStartKeyboard.Widget(""),
@ -150,7 +153,7 @@ var beh = tg.NewBehaviour().
), ),
), ),
tg.NewScreen("start/send-location", tg.NewPage( tg.NewScreen("/start/send-location", tg.NewPage(
"", "",
).WithReply( ).WithReply(
sendLocationKeyboard.Widget("Press the button to send your location!"), sendLocationKeyboard.Widget("Press the button to send your location!"),
@ -172,7 +175,7 @@ var beh = tg.NewBehaviour().
Desc("start or restart the bot or move to the start screen"). Desc("start or restart the bot or move to the start screen").
ActionFunc(func(c *tg.Context){ ActionFunc(func(c *tg.Context){
c.Sendf("Your username is %q", c.Message.From.UserName) c.Sendf("Your username is %q", c.Message.From.UserName)
c.ChangeScreen("start") c.ChangeScreen("/start")
}), }),
tg.NewCommand("hello"). tg.NewCommand("hello").
Desc("sends the 'Hello, World!' message back"). Desc("sends the 'Hello, World!' message back").
@ -181,9 +184,9 @@ var beh = tg.NewBehaviour().
}), }),
tg.NewCommand("read"). tg.NewCommand("read").
Desc("reads a string and sends it back"). Desc("reads a string and sends it back").
WidgetFunc(func(c *tg.Context, updates *tg.UpdateChan) { WidgetFunc(func(c *tg.Context) {
c.Sendf("Type text and I will send it back to you") c.Sendf("Type text and I will send it back to you")
for u := range updates.Chan() { for u := range c.Input() {
if u.Message == nil { if u.Message == nil {
continue continue
} }

View file

@ -233,7 +233,8 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
go (&Context{ go (&Context{
context: ctx, context: ctx,
Update: u, Update: u,
}).Serve(chn) input: chn,
}).serve()
} }
} else if u.Message != nil { } else if u.Message != nil {
// Create session on any message // Create session on any message
@ -249,7 +250,8 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
go (&Context{ go (&Context{
context: ctx, context: ctx,
Update: u, Update: u,
}).Serve(chn) input: chn,
}).serve()
} }
chn, ok := chans[sid] chn, ok := chans[sid]

View file

@ -153,10 +153,7 @@ func (widget *Command) Filter(
return false return false
} }
func (widget *CommandWidget) Serve( func (widget *CommandWidget) Serve(c *Context) {
c *Context,
updates *UpdateChan,
) {
commanders := make(map[CommandName] BotCommander) commanders := make(map[CommandName] BotCommander)
for k, v := range widget.Commands { for k, v := range widget.Commands {
commanders[k] = v commanders[k] = v
@ -167,7 +164,7 @@ func (widget *CommandWidget) Serve(
) )
var cmdUpdates *UpdateChan var cmdUpdates *UpdateChan
for u := range updates.Chan() { for u := range c.Input() {
if c.ScreenId() == "" && u.Message != nil { if c.ScreenId() == "" && u.Message != nil {
// Skipping and executing the preinit action // Skipping and executing the preinit action
// while we have the empty screen. // while we have the empty screen.
@ -190,20 +187,12 @@ func (widget *CommandWidget) Serve(
c.Run(cmd.Action, u) c.Run(cmd.Action, u)
if cmd.Widget != nil { if cmd.Widget != nil {
cmdUpdates.Close() cmdUpdates.Close()
cmdUpdates = NewUpdateChan() cmdUpdates = c.RunWidget(cmd.Widget)
go func() {
cmd.Widget.Serve(
&Context{context: c.context, Update: u},
cmdUpdates,
)
cmdUpdates.Close()
cmdUpdates = nil
}()
} }
continue continue
} }
if cmdUpdates != nil { if !cmdUpdates.Closed() {
// Send to the commands channel if we are // Send to the commands channel if we are
// executing one. // executing one.
cmdUpdates.Send(u) cmdUpdates.Send(u)

View file

@ -6,6 +6,7 @@ import (
// Simple text message type. // Simple text message type.
type MessageConfig struct { type MessageConfig struct {
ParseMode string
Text string Text string
} }
@ -13,14 +14,35 @@ type MessageConfig struct {
func NewMessage(text string) *MessageConfig { func NewMessage(text string) *MessageConfig {
ret := &MessageConfig{} ret := &MessageConfig{}
ret.Text = text ret.Text = text
ret.ParseMode = tgbotapi.ModeMarkdown
return ret return ret
} }
func (msg *MessageConfig) withParseMode(mode string) *MessageConfig{
msg.ParseMode = mode
return msg
}
// Set the default Markdown parsing mode.
func (msg *MessageConfig) MD() *MessageConfig {
return msg.withParseMode(tgbotapi.ModeMarkdown)
}
func (msg *MessageConfig) MD2() *MessageConfig {
return msg.withParseMode(tgbotapi.ModeMarkdownV2)
}
func (msg *MessageConfig) HTML() *MessageConfig {
return msg.withParseMode(tgbotapi.ModeHTML)
}
func (config *MessageConfig) SendConfig( func (config *MessageConfig) SendConfig(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) (*SendConfig) { ) (*SendConfig) {
var ret SendConfig var ret SendConfig
msg := tgbotapi.NewMessage(sid.ToApi(), config.Text) msg := tgbotapi.NewMessage(sid.ToApi(), config.Text)
ret.Message = &msg ret.Message = &msg
ret.Message.ParseMode = config.ParseMode
return &ret return &ret
} }

View file

@ -85,26 +85,24 @@ func (p *Page) Filter(
return false return false
} }
func (p *Page) Serve( func (p *Page) Serve(c *Context) {
c *Context, updates *UpdateChan,
) {
msgs, _ := c.Render(p)
inlineMsg := msgs["page/inline"]
if p.Action != nil { if p.Action != nil {
c.Run(p.Action, c.Update) c.Run(p.Action, c.Update)
} }
msgs, _ := c.Render(p)
inlineMsg := msgs["page/inline"]
subUpdates := c.RunWidgetBg(p.SubWidget) subUpdates := c.RunWidget(p.SubWidget)
defer subUpdates.Close() defer subUpdates.Close()
inlineUpdates := c.RunWidgetBg(p.Inline) inlineUpdates := c.RunWidget(p.Inline)
defer inlineUpdates.Close() defer inlineUpdates.Close()
replyUpdates := c.RunWidgetBg(p.Reply) replyUpdates := c.RunWidget(p.Reply)
defer replyUpdates.Close() defer replyUpdates.Close()
subFilter, subFilterOk := p.SubWidget.(Filterer) subFilter, subFilterOk := p.SubWidget.(Filterer)
for u := range updates.Chan() { for u := range c.Input() {
switch { switch {
case !p.Inline.Filter(u, MessageMap{"": inlineMsg}) : case !p.Inline.Filter(u, MessageMap{"": inlineMsg}) :
inlineUpdates.Send(u) inlineUpdates.Send(u)

View file

@ -6,6 +6,18 @@ import (
//tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" //tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
type ContextType string
const (
NoContextType = iota
PrivateContextType
GroupContextType
ChannelContextType
)
// General context for a specific user.
// Is always the same and is not reached
// inside end function-handlers.
type context struct { type context struct {
Session *Session Session *Session
// To reach the bot abilities inside callbacks. // To reach the bot abilities inside callbacks.
@ -15,17 +27,13 @@ type context struct {
screenId, prevScreenId ScreenId screenId, prevScreenId ScreenId
} }
// The type represents way to interact with user in
// handling functions. Is provided to Act() function always.
// Goroutie function to handle each user. // Goroutie function to handle each user.
func (c *Context) Serve(updates *UpdateChan) { func (c *Context) serve() {
beh := c.Bot.behaviour beh := c.Bot.behaviour
if beh.Init != nil { if beh.Init != nil {
c.Run(beh.Init, c.Update) c.Run(beh.Init, c.Update)
} }
beh.Root.Serve(c, updates) beh.Root.Serve(c)
} }
@ -70,6 +78,14 @@ func (c *Context) Sendf(format string, v ...any) (*Message, error) {
return c.Send(NewMessage(fmt.Sprintf(format, v...))) return c.Send(NewMessage(fmt.Sprintf(format, v...)))
} }
func (c *Context) Sendf2(format string, v ...any) (*Message, error) {
return c.Send(NewMessage(fmt.Sprintf(format, v...)).MD2())
}
func (c *Context) SendfHTML(format string, v ...any) (*Message, error) {
return c.Send(NewMessage(fmt.Sprintf(format, v...)).HTML())
}
// Interface to interact with the user. // Interface to interact with the user.
type Context struct { type Context struct {
*context *context
@ -78,9 +94,43 @@ type Context struct {
// Used as way to provide outer values redirection // Used as way to provide outer values redirection
// into widgets and actions. It is like arguments // into widgets and actions. It is like arguments
// for REST API request etc. // for REST API request etc.
Args []any Arg any
// Instead of updates as argument.
input *UpdateChan
} }
// Get the input for current widget.
// Should be used inside handlers (aka "Serve").
func (c *Context) Input() chan *Update {
return c.input.Chan()
}
// Returns copy of current context so
// it will not affect the current one.
// But be careful because
// most of the insides uses pointers
// which are not deeply copied.
func (c *Context) Copy() *Context {
ret := *c
return &ret
}
func (c *Context) WithArg(v any) *Context {
c.Arg = v
return c
}
func (c *Context) WithUpdate(u *Update) *Context {
c.Update = u
return c
}
func (c *Context) WithInput(input *UpdateChan) *Context {
c.input = input
return c
}
// Customized actions for the bot. // Customized actions for the bot.
type Action interface { type Action interface {
Act(*Context) Act(*Context)
@ -121,19 +171,9 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
// Stopping the current widget. // Stopping the current widget.
c.skippedUpdates.Close() c.skippedUpdates.Close()
// Making channel for the new widget. c.skippedUpdates = nil
c.skippedUpdates = NewUpdateChan()
if screen.Widget != nil { if screen.Widget != nil {
// Running the widget if the screen has one. c.skippedUpdates = c.RunWidget(screen.Widget, args)
go func() {
updates := c.skippedUpdates
screen.Widget.Serve(&Context{
context: c.context,
Update: c.Update,
Args: args,
}, updates)
updates.Close()
}()
} else { } else {
panic("no widget defined for the screen") panic("no widget defined for the screen")
} }
@ -141,6 +181,33 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
return nil return nil
} }
// Run widget in background returning the new input channel for it.
func (c *Context) RunWidget(widget Widget, args ...any) *UpdateChan {
if widget == nil {
return nil
}
var arg any
if len(args) == 1 {
arg = args[0]
} else if len(args) > 1 {
arg = args
}
updates := NewUpdateChan()
go func() {
widget.Serve(
c.Copy().
WithInput(updates).
WithArg(arg),
)
updates.Close()
}()
return updates
}
func (c *Context) ChangeToPrevScreen() { func (c *Context) ChangeToPrevScreen() {
c.ChangeScreen(c.PrevScreenId()) c.ChangeScreen(c.PrevScreenId())
} }

View file

@ -13,7 +13,7 @@ type Widget interface {
// widget MUST end its work. // widget MUST end its work.
// Mostly made by looping over the // Mostly made by looping over the
// updates range. // updates range.
Serve(*Context, *UpdateChan) Serve(*Context)
} }
// Needs implementation. // Needs implementation.
@ -49,11 +49,13 @@ func (updates *UpdateChan) Chan() chan *Update {
} }
// Send an update to the channel. // Send an update to the channel.
func (updates *UpdateChan) Send(u *Update) { // Returns true if the update was sent.
if updates != nil && updates.chn == nil { func (updates *UpdateChan) Send(u *Update) bool {
return if updates == nil || updates.chn == nil {
return false
} }
updates.chn <- u updates.chn <- u
return true
} }
// Read an update from the channel. // Read an update from the channel.
@ -66,7 +68,7 @@ func (updates *UpdateChan) Read() *Update {
// Returns true if the channel is closed. // Returns true if the channel is closed.
func (updates *UpdateChan) Closed() bool { func (updates *UpdateChan) Closed() bool {
return updates.chn == nil return updates==nil || updates.chn == nil
} }
// Close the channel. Used in defers. // Close the channel. Used in defers.
@ -78,16 +80,6 @@ func (updates *UpdateChan) Close() {
updates.chn = nil updates.chn = nil
} }
func (c *Context) RunWidgetBg(widget Widget) *UpdateChan {
if widget == nil {
return nil
}
updates := NewUpdateChan()
go widget.Serve(c, updates)
return updates
}
// Implementing the interface provides // Implementing the interface provides
type DynamicWidget interface { type DynamicWidget interface {
@ -96,10 +88,10 @@ type DynamicWidget interface {
// The function that implements the Widget // The function that implements the Widget
// interface. // interface.
type WidgetFunc func(*Context, *UpdateChan) type WidgetFunc func(*Context)
func (wf WidgetFunc) Serve(c *Context, updates *UpdateChan) { func (wf WidgetFunc) Serve(c *Context) {
wf(c, updates) wf(c)
} }
func (wf WidgetFunc) Filter( func (wf WidgetFunc) Filter(
@ -145,11 +137,8 @@ func (widget *InlineKeyboardWidget) SendConfig(
return ret return ret
} }
func (widget *InlineKeyboardWidget) Serve( func (widget *InlineKeyboardWidget) Serve(c *Context) {
c *Context, for u := range c.Input() {
updates *UpdateChan,
) {
for u := range updates.Chan() {
var act Action var act Action
if u.CallbackQuery == nil { if u.CallbackQuery == nil {
continue continue
@ -263,11 +252,8 @@ func (widget *ReplyKeyboardWidget) Filter(
return false return false
} }
func (widget *ReplyKeyboardWidget) Serve( func (widget *ReplyKeyboardWidget) Serve(c *Context) {
c *Context, for u := range c.Input() {
updates *UpdateChan,
) {
for u := range updates.Chan() {
var btn *Button var btn *Button
text := u.Message.Text text := u.Message.Text
btns := widget.ButtonMap() btns := widget.ButtonMap()