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
}
func (w *MutateMessageWidget) Serve(c *tg.Context, updates *tg.UpdateChan) {
for _, arg := range c.Args {
c.Sendf("%v", arg)
func (w *MutateMessageWidget) Serve(c *tg.Context) {
args, ok := c.Arg.([]any)
if ok {
for _, arg := range args {
c.Sendf("%v", arg)
}
}
for u := range updates.Chan() {
for u := range c.Input() {
text := u.Message.Text
c.Sendf("%s", w.Mutate(text))
}
@ -49,7 +52,7 @@ func ExtractSessionData(c *tg.Context) *SessionData {
var (
startScreenButton = tg.NewButton("🏠 To the start screen").
ScreenChange("start")
ScreenChange("/start")
incDecKeyboard = tg.NewKeyboard().Row(
tg.NewButton("+").ActionFunc(func(c *tg.Context) {
@ -67,14 +70,14 @@ var (
)
navKeyboard = tg.NewKeyboard().Row(
tg.NewButton("Inc/Dec").ScreenChange("start/inc-dec"),
tg.NewButton("Inc/Dec").ScreenChange("/start/inc-dec"),
).Row(
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(
tg.NewButton("Send location").ScreenChange("start/send-location"),
tg.NewButton("Send location").ScreenChange("/start/send-location"),
).Reply().WithOneTime(true)
sendLocationKeyboard = tg.NewKeyboard().Row(
@ -107,7 +110,7 @@ var beh = tg.NewBehaviour().
// The session initialization.
c.Session.Data = &SessionData{}
}).WithScreens(
tg.NewScreen("start", tg.NewPage(
tg.NewScreen("/start", tg.NewPage(
"",
).WithInline(
tg.NewKeyboard().Row(
@ -118,7 +121,7 @@ var beh = tg.NewBehaviour().
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 "+
"user separated data works "+
"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",
).WithReply(
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",
).WithReply(
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(
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").
ActionFunc(func(c *tg.Context){
c.Sendf("Your username is %q", c.Message.From.UserName)
c.ChangeScreen("start")
c.ChangeScreen("/start")
}),
tg.NewCommand("hello").
Desc("sends the 'Hello, World!' message back").
@ -181,9 +184,9 @@ var beh = tg.NewBehaviour().
}),
tg.NewCommand("read").
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")
for u := range updates.Chan() {
for u := range c.Input() {
if u.Message == nil {
continue
}

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import (
// Simple text message type.
type MessageConfig struct {
ParseMode string
Text string
}
@ -13,14 +14,35 @@ type MessageConfig struct {
func NewMessage(text string) *MessageConfig {
ret := &MessageConfig{}
ret.Text = text
ret.ParseMode = tgbotapi.ModeMarkdown
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(
sid SessionId, bot *Bot,
) (*SendConfig) {
var ret SendConfig
msg := tgbotapi.NewMessage(sid.ToApi(), config.Text)
ret.Message = &msg
ret.Message.ParseMode = config.ParseMode
return &ret
}

View file

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

View file

@ -6,6 +6,18 @@ import (
//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 {
Session *Session
// To reach the bot abilities inside callbacks.
@ -15,17 +27,13 @@ type context struct {
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.
func (c *Context) Serve(updates *UpdateChan) {
func (c *Context) serve() {
beh := c.Bot.behaviour
if beh.Init != nil {
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...)))
}
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.
type Context struct {
*context
@ -78,9 +94,43 @@ type Context struct {
// Used as way to provide outer values redirection
// into widgets and actions. It is like arguments
// 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.
type Action interface {
Act(*Context)
@ -121,19 +171,9 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
// Stopping the current widget.
c.skippedUpdates.Close()
// Making channel for the new widget.
c.skippedUpdates = NewUpdateChan()
c.skippedUpdates = nil
if screen.Widget != nil {
// Running the widget if the screen has one.
go func() {
updates := c.skippedUpdates
screen.Widget.Serve(&Context{
context: c.context,
Update: c.Update,
Args: args,
}, updates)
updates.Close()
}()
c.skippedUpdates = c.RunWidget(screen.Widget, args)
} else {
panic("no widget defined for the screen")
}
@ -141,6 +181,33 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
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() {
c.ChangeScreen(c.PrevScreenId())
}

View file

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