diff --git a/cmd/test/main.go b/cmd/test/main.go index 4a91929..23603ec 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -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 } diff --git a/tg/bot.go b/tg/bot.go index a48cfae..07f06f9 100644 --- a/tg/bot.go +++ b/tg/bot.go @@ -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] diff --git a/tg/command.go b/tg/command.go index d18c69c..a9ed948 100644 --- a/tg/command.go +++ b/tg/command.go @@ -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) diff --git a/tg/message.go b/tg/message.go index 55cd0ab..206fa3c 100644 --- a/tg/message.go +++ b/tg/message.go @@ -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 } diff --git a/tg/page.go b/tg/page.go index b6da7f2..076d556 100644 --- a/tg/page.go +++ b/tg/page.go @@ -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) diff --git a/tg/private.go b/tg/private.go index a6af20c..053d7b4 100644 --- a/tg/private.go +++ b/tg/private.go @@ -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()) } diff --git a/tg/widget.go b/tg/widget.go index e9afe48..e0ce98b 100644 --- a/tg/widget.go +++ b/tg/widget.go @@ -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()