From 3f2f16a79e317b73313e252c14ffdb828b6e1ba1 Mon Sep 17 00:00:00 2001 From: surdeus Date: Thu, 28 Mar 2024 12:41:09 +0500 Subject: [PATCH] save the work about getting rid of pointers. --- action.go | 13 --- bot.go | 65 +++++++------ button.go | 26 +++-- command.go | 44 ++++----- context.go | 266 +++++++++------------------------------------------- file.go | 6 +- go.go | 22 +++++ inline.go | 23 +++-- keyboard.go | 25 ++--- message.go | 55 ++++++----- reply.go | 21 +++-- send.go | 30 +----- session.go | 147 +++++++++++++++++++++++++++-- update.go | 18 ++-- 14 files changed, 348 insertions(+), 413 deletions(-) delete mode 100644 action.go create mode 100644 go.go diff --git a/action.go b/action.go deleted file mode 100644 index 498580b..0000000 --- a/action.go +++ /dev/null @@ -1,13 +0,0 @@ -package tg - -import ( - //"reflect" -) - -// The argument for handling in channenl behaviours. -type ChannelContext struct { -} -type CC = ChannelContext -type ChannelAction struct { - Act (*ChannelContext) -} diff --git a/bot.go b/bot.go index b54db31..fb6af85 100644 --- a/bot.go +++ b/bot.go @@ -13,16 +13,15 @@ type User = tgbotapi.User // The wrapper around Telegram API. type Bot struct { // Custom data value. - Data any - Api *tgbotapi.BotAPI - Me *User + data any + api *tgbotapi.BotAPI + me User // Private bot behaviour. behaviour *Behaviour // Group bot behaviour. //groupBehaviour *GroupBehaviour // Bot behaviour in channels. //channelBehaviour *ChannelBehaviour - contexts map[SessionId] *context sessions SessionMap //groupSessions GroupSessionMap } @@ -35,40 +34,49 @@ func NewBot(token string) (*Bot, error) { } return &Bot{ - Api: bot, - contexts: make(map[SessionId] *context), + api: bot, }, nil } func (bot *Bot) Debug(debug bool) *Bot { - bot.Api.Debug = debug + bot.api.Debug = debug return bot } +func (bot *Bot) Api() *tgbotapi.BotAPI { + return bot.api +} + +func (bot *Bot) Me() User { + return bot.me +} + // Send the Renderable to the specified session client side. // Can be used for both group and private sessions because // SessionId represents both for chat IDs. func (bot *Bot) Send( sid SessionId, v Sendable, -) (*Message, error) { +) (Message, error) { config := v.SendConfig(sid, bot) if config.Error != nil { - return nil, config.Error + return Message{}, config.Error } - msg, err := bot.Api.Send(config.ToApi()) + msg, err := bot.api.Send(config.ToApi()) if err != nil { - return nil, err + return Message{}, err } - return &msg, nil + v.SetMessage(msg) + return msg, nil } func (bot *Bot) Sendf( sid SessionId, format string, v ...any, -) (*Message, error){ +) (Message, error){ + msg := Messagef(format, v...) return bot.Send( sid, - NewMessage(format, v...), + &msg, ) } @@ -76,7 +84,7 @@ func (bot *Bot) Sendf( func (bot *Bot) SendRaw( sid SessionId, v tgbotapi.Chattable, ) (*Message, error) { - msg, err := bot.Api.Send(v) + msg, err := bot.api.Send(v) if err != nil { return nil, err } @@ -117,7 +125,7 @@ func (b *Bot) WithGroupSessions(sessions GroupSessionMap) *Bot { func (bot *Bot) DeleteCommands() { //tgbotapi.NewBotCommandScopeAllPrivateChats(), cfg := tgbotapi.NewDeleteMyCommands() - bot.Api.Request(cfg) + bot.api.Request(cfg) } // Setting the command on the user side. @@ -151,7 +159,7 @@ func (bot *Bot) SetCommands( botCmds..., ) - _, err := bot.Api.Request(cfg) + _, err := bot.api.Request(cfg) if err != nil { return err } @@ -170,11 +178,11 @@ func (bot *Bot) Run() error { uc := tgbotapi.NewUpdate(0) uc.Timeout = 10 - updates := bot.Api.GetUpdatesChan(uc) - handles := make(map[string] chan *Update) + updates := bot.api.GetUpdatesChan(uc) + handles := make(map[string] chan Update) if bot.behaviour != nil { - chn := make(chan *Update) + chn := make(chan Update) handles["private"] = chn go bot.handlePrivate(chn) } @@ -195,10 +203,10 @@ func (bot *Bot) Run() error { }*/ me, _ := bot.Api.GetMe() - bot.Me = &me + bot.me = me for up := range updates { - u := &Update{ - Update: &up, + u := Update{ + Update: up, } // Sometimes returns nil. @@ -220,7 +228,7 @@ func (bot *Bot) Run() error { // The function handles updates supposed for the private // chat with the bot. -func (bot *Bot) handlePrivate(updates chan *Update) { +func (bot *Bot) handlePrivate(updates chan Update) { var sid SessionId for u := range updates { sid = SessionId(u.FromChat().ID) @@ -245,12 +253,13 @@ func (bot *Bot) handlePrivate(updates chan *Update) { bot.contexts[sid] = ctx } - go (&Context{ - context: ctx, + go Context{ + session: session, + bot: bot, Update: u, input: ctx.updates, - }).serve() - ctx.updates.Send(u) + }.serve() + ctx.session.updates.Send(u) continue } diff --git a/button.go b/button.go index 0e3caa2..4573bb8 100644 --- a/button.go +++ b/button.go @@ -16,7 +16,7 @@ type Button struct { Action Action } -type ButtonMap map[string]*Button +type ButtonMap map[string]Button // Returns the only location button in the map. func (btnMap ButtonMap) LocationButton() *Button { @@ -32,14 +32,14 @@ func (btnMap ButtonMap) LocationButton() *Button { type ButtonRow []*Button // Returns new button with the specified text and no action. -func NewButton(format string, v ...any) *Button { +func Buttonf(format string, v ...any) Button { return &Button{ Text: fmt.Sprintf(format, v...), } } // Randomize buttons data to make the key unique. -func (btn *Button) Rand() *Button { +func (btn Button) Rand() Button { rData := make([]byte, 8) rand.Read(rData) data := make([]byte, base64.StdEncoding.EncodedLen(len(rData))) @@ -49,41 +49,37 @@ func (btn *Button) Rand() *Button { } // Set the URL for the button. Only for inline buttons. -func (btn *Button) WithUrl(format string, v ...any) *Button { +func (btn Button) WithUrl(format string, v ...any) Button { btn.Url = fmt.Sprintf(format, v...) return btn } // Set the action when pressing the button. // By default is nil and does nothing. -func (btn *Button) WithAction(a Action) *Button { +func (btn Button) WithAction(a Action) Button { btn.Action = a return btn } -func (btn *Button) WithData(dat string) *Button { +func (btn Button) WithData(dat string) Button { btn.Data = dat return btn } // Sets whether the button must send owner's location. -func (btn *Button) WithSendLocation(ok bool) *Button { +func (btn Button) WithSendLocation(ok bool) Button { btn.SendLocation = ok return btn } -func (btn *Button) ActionFunc(fn ActionFunc) *Button { - return btn.WithAction(fn) -} - -func (btn *Button) Go(pth Path, args ...any) *Button { +func (btn Button) Go(pth Path, args ...any) Button { return btn.WithAction(ScreenGo{ Path: pth, Args: args, }) } -func (btn *Button) ToTelegram() apix.KeyboardButton { +func (btn Button) ToTelegram() apix.KeyboardButton { ret := apix.NewKeyboardButton(btn.Text) if btn.SendLocation { ret.RequestLocation = true @@ -91,7 +87,7 @@ func (btn *Button) ToTelegram() apix.KeyboardButton { return ret } -func (btn *Button) ToTelegramInline() apix.InlineKeyboardButton { +func (btn Button) ToTelegramInline() apix.InlineKeyboardButton { if btn.Data != "" { return apix.NewInlineKeyboardButtonData(btn.Text, btn.Data) } @@ -105,7 +101,7 @@ func (btn *Button) ToTelegramInline() apix.InlineKeyboardButton { } // Return the key of the button to identify it by messages and callbacks. -func (btn *Button) Key() string { +func (btn Button) Key() string { if btn == nil { return "" } diff --git a/command.go b/command.go index bfe7430..b1ea1c1 100644 --- a/command.go +++ b/command.go @@ -1,8 +1,6 @@ package tg import ( - //"flag" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) @@ -14,17 +12,17 @@ const ( ) type CommandName string - type Command struct { - Name CommandName + Name CommandName Type CommandType Description string - Action Action + Action Action Widget Widget } + type CommandMap map[CommandName]*Command -func NewCommand(name CommandName, desc string) *Command { +func NewCommand(name CommandName, desc string) Command { if name == "" || desc == "" { panic("name and description cannot be an empty string") } @@ -39,27 +37,23 @@ func (c *Command) WithAction(a Action) *Command { return c } -func (c *Command) ActionFunc(af ActionFunc) *Command { - return c.WithAction(af) -} - -func (c *Command) WithWidget(w Widget) *Command { +func (c Command) WithWidget(w Widget) Command { c.Widget = w return c } -func (c *Command) WidgetFunc(fn Func) *Command { +func (c Command) WidgetFunc(fn Func) Command { return c.WithWidget(fn) } -func (c *Command) ToApi() tgbotapi.BotCommand { +func (c Command) ToApi() tgbotapi.BotCommand { ret := tgbotapi.BotCommand{} ret.Command = string(c.Name) ret.Description = c.Description return ret } -func (c *Command) Go(pth Path, args ...any) *Command { +func (c Command) Go(pth Path, args ...any) Command { return c.WithAction(ScreenGo{ Path: pth, Args: args, @@ -76,13 +70,12 @@ type CommandCompo struct { // Returns new empty CommandCompo. func NewCommandCompo(cmds ...*Command) *CommandCompo { - ret := (&CommandCompo{}).WithCommands(cmds...) - //ret.Commands = make(CommandMap) + ret := CommandCompo{}.WithCommands(cmds...) return ret } // Set the commands to handle. -func (w *CommandCompo) WithCommands(cmds ...*Command) *CommandCompo { +func (w CommandCompo) WithCommands(cmds ...*Command) *CommandCompo { if w.Commands == nil { w.Commands = make(CommandMap) } @@ -105,23 +98,18 @@ func (w *CommandCompo) WithPreStart(a Action) *CommandCompo { return w } -// Set the prestart action with function. -func (w *CommandCompo) WithPreStartFunc(fn ActionFunc) *CommandCompo { - return w.WithPreStart(fn) -} - // Set the usage action. -func (w *CommandCompo) WithUsage(a Action) *CommandCompo { +func (w CommandCompo) WithUsage(a Action) *CommandCompo { w.Usage = a return w } // Set the usage action with function. -func (w *CommandCompo) WithUsageFunc(fn ActionFunc) *CommandCompo { +func (w CommandCompo) WithUsageFunc(fn ActionFunc) *CommandCompo { return w.WithUsage(fn) } -func (widget *CommandCompo) Filter( +func (widget CommandCompo) Filter( u *Update, ) bool { if u.Message == nil || !u.Message.IsCommand() { @@ -132,13 +120,13 @@ func (widget *CommandCompo) Filter( } // Implementing server. -func (compo *CommandCompo) Serve(c *Context) { +func (compo CommandCompo) Serve(c Context) { /*commanders := make(map[CommandName] BotCommander) for k, v := range compo.Commands { commanders[k] = v }*/ - c.Bot.DeleteCommands() - err := c.Bot.SetCommands( + c.bot.DeleteCommands() + err := c.bot.SetCommands( tgbotapi.NewBotCommandScopeChat(c.Session.Id.ToApi()), compo.Commands, ) diff --git a/context.go b/context.go index bb83e15..62d7ebf 100644 --- a/context.go +++ b/context.go @@ -8,57 +8,17 @@ import ( //"path" ) -func Go(pth Path) UI { - return UI{ - GoWidget(pth), - } -} - -type GoWidget string -// Implementing the Server interface. -func (widget GoWidget) Serve(c *Context) { - c.input.Close() - c.Go(Path(widget)) -} - -func (widget GoWidget) Render(c *Context) UI { - return UI{widget} -} - -func (widget GoWidget) Filter(u *Update) bool { - return true -} - -// 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. - Bot *Bot - // Costum status for currently running context. - Status any - Type ContextType - updates *UpdateChan - skippedUpdates *UpdateChan - // Current screen ID. - pathHistory []Path - //path, prevPath Path -} - -type Contexter interface { - GetContext() *Context -} // Interface to interact with the user. type Context struct { - *context + session *Session // The update that called the Context usage. - *Update + update Update // Used as way to provide outer values redirection // into widgets and actions. It is like arguments // for REST API request etc. arg any + typ ContextType // Instead of updates as argument. input *UpdateChan } @@ -68,7 +28,7 @@ type Context struct { // maybe you will find another usage for this. // Returns users context by specified session ID // or nil if the user is not logged in. -func (c *Context) As(sid SessionId) *Context { +func (c Context) As(sid SessionId) Context { n, ok := c.Bot.contexts[sid] if !ok { return nil @@ -78,17 +38,13 @@ func (c *Context) As(sid SessionId) *Context { } } -func (c *Context) GetContext() *Context { - return c -} - // General type function to define actions, single component widgets // and components themselves. -type Func func(*Context) -func (f Func) Act(c *Context) { +type Func func(Context) +func (f Func) Act(c Context) { f(c) } -func (f Func) Serve(c *Context) { +func (f Func) Serve(c Context) { f(c) } func(f Func) Filter(_ *Update) bool { @@ -108,13 +64,13 @@ const ( ) // Goroutie function to handle each user. -func (c *Context) serve() { +func (c Context) serve() { beh := c.Bot.behaviour c.Run(beh.Init) beh.Root.Serve(c) } -func (c *Context) Path() Path { +func (c Context) Path() Path { ln := len(c.pathHistory) if ln == 0 { return "" @@ -122,11 +78,11 @@ func (c *Context) Path() Path { return c.pathHistory[ln-1] } -func (c *Context) Arg() any { +func (c Context) Arg() any { return c.arg } -func (c *Context) Run(a Action) { +func (c Context) Run(a Action) { if a != nil { a.Act(c) } @@ -135,12 +91,12 @@ func (c *Context) Run(a Action) { // Only for the root widget usage. // Skip the update sending it down to // the underlying widget. -func (c *Context) Skip(u *Update) { +func (c Context) Skip(u Update) { c.skippedUpdates.Send(u) } // Sends to the Sendable object. -func (c *Context) Send(v Sendable) (*Message, error) { +func (c Context) Send(v Sendable) (Message, error) { config := v.SendConfig(c.Session.Id, c.Bot) if config.Error != nil { return nil, config.Error @@ -155,219 +111,79 @@ func (c *Context) Send(v Sendable) (*Message, error) { // Sends the formatted with fmt.Sprintf message to the user // using default Markdown parsing format. -func (c *Context) Sendf(format string, v ...any) (*Message, error) { +func (c Context) Sendf(format string, v ...any) (Message, error) { return c.Send(NewMessage(format, v...)) } // Same as Sendf but uses Markdown 2 format for parsing. -func (c *Context) Sendf2(format string, v ...any) (*Message, error) { +func (c Context) Sendf2(format string, v ...any) (Message, error) { return c.Send(NewMessage(fmt.Sprintf(format, v...)).MD2()) } // Same as Sendf but uses HTML format for parsing. -func (c *Context) SendfHTML(format string, v ...any) (*Message, error) { +func (c Context) SendfHTML(format string, v ...any) (Message, error) { return c.Send(NewMessage(fmt.Sprintf(format, v...)).HTML()) } -func (c *Context) SendfR(format string, v ...any) (*Message, error) { +// Send the message in raw format escaping all the special characters. +func (c Context) SendfR(format string, v ...any) (Message, error) { return c.Send(NewMessage(Escape2(fmt.Sprintf(format, v...))).MD2()) } // Get the input for current widget. // Should be used inside handlers (aka "Serve"). -func (c *Context) Input() chan *Update { +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 = c.Copy() +func (c Context) WithArg(v any) Context { c.arg = v return c } -func (c *Context) WithUpdate(u *Update) *Context { - c = c.Copy() +func (c Context) WithUpdate(u *Update) Context { c.Update = u return c } -func (c *Context) WithInput(input *UpdateChan) *Context { - c = c.Copy() +func (c Context) WithInput(input *UpdateChan) Context { c.input = input return c } +func (c Context) Go(pth Path) error { + return c.session.go_(pth, nil) +} + +func (c Context) GoWithArg(pth Path, arg any) error { + return c.session.go_(pth, arg) +} // Customized actions for the bot. type Action interface { - Act(*Context) + Act(Context) } -type ActionFunc func(*Context) +type ActionFunc func(Context) func (af ActionFunc) Act(c *Context) { af(c) } -func (c *Context) History() []Path { - return c.pathHistory +func (c Context) History() []Path { + return c.session.pathHistory } -// Changes screen of user to the Id one. -func (c *Context) Go(pth Path, args ...any) error { - var err error - if pth == "" { - c.pathHistory = []Path{} - return nil - } - var back bool - if pth == "-" { - if len(c.pathHistory) < 2 { - return c.Go("") - } - pth = c.pathHistory[len(c.pathHistory)-2] - c.pathHistory = c.pathHistory[:len(c.pathHistory)-1] - } - // Getting the screen and changing to - // then executing its widget. - if !pth.IsAbs() { - pth = (c.Path() + "/" + pth).Clean() - } - - if !c.PathExist(pth) { - return ScreenNotExistErr - } - - if !back && c.Path() != pth { - c.pathHistory = append(c.pathHistory, pth) - } - - // Stopping the current widget. - screen := c.Bot.behaviour.Screens[pth] - c.skippedUpdates.Close() - if screen.Widget != nil { - c.skippedUpdates, err = c.RunWidget(screen.Widget, args...) - if err != nil { - return err - } - } else { - return NoWidgetForScreenErr - } - - return nil +func (c Context) PathExist(pth Path) bool { + return c.bot.behaviour.PathExist(pth) } -func (c *Context) PathExist(pth Path) bool { - return c.Bot.behaviour.PathExist(pth) -} - -func (c *Context) makeArg(args []any) any { - var arg any - if len(args) == 1 { - arg = args[0] - } else if len(args) > 1 { - arg = args - } - return arg -} - -func (c *Context) RunCompo(compo Component, args ...any) (*UpdateChan, error) { - if compo == nil { - return nil, nil - } - s, ok := compo.(Sendable) - if ok { - msg, err := c.Send(s) - if err != nil { - return nil, err - } - s.SetMessage(msg) - } - updates := NewUpdateChan() - go func() { - compo.Serve( - c.WithInput(updates). - WithArg(c.makeArg(args)), - ) - // To let widgets finish themselves before - // the channel is closed and close it by themselves. - updates.Close() - }() - return updates, nil -} - -// Run widget in background returning the new input channel for it. -func (c *Context) RunWidget(widget Widget, args ...any) (*UpdateChan, error) { - var err error - if widget == nil { - return nil, EmptyWidgetErr - } - - pth := c.Path() - compos := widget.Render(c.WithArg(c.makeArg(args))) - // Leave if changed path or components are empty. - if compos == nil || pth != c.Path() { - return nil, EmptyCompoErr - } - chns := make([]*UpdateChan, len(compos)) - for i, compo := range compos { - chns[i], err = c.RunCompo(compo, args...) - if err != nil { - for _, chn := range chns { - chn.Close() - } - return nil, err - } - } - - ret := NewUpdateChan() - go func() { - ln := len(compos) - UPDATE: - for u := range ret.Chan() { - if u == nil { - break - } - cnt := 0 - for i, compo := range compos { - chn := chns[i] - if chn.Closed() { - cnt++ - continue - } - if !compo.Filter(u) { - chn.Send(u) - continue UPDATE - } - } - if cnt == ln { - break - } - } - ret.Close() - for _, chn := range chns { - chn.Close() - } - }() - - return ret, nil -} - -// Simple way to read strings for widgets. -func (c *Context) ReadString(pref string, args ...any) string { +// Simple way to read strings for widgets with +// the specified prompt. +func (c Context) ReadString(promptf string, args ...any) string { var text string if pref != "" { - c.Sendf(pref, args...) + c.Sendf(promptf, args...) } for u := range c.Input() { if u == nil { @@ -382,6 +198,10 @@ func (c *Context) ReadString(pref string, args ...any) string { return text } +func (c Context) Update() Update { + return c.update +} + // Returns the reader for specified file ID and path. func (c *Context) GetFile(fileId FileId) (io.ReadCloser, string, error) { file, err := c.Bot.Api.GetFile(tgbotapi.FileConfig{FileID:string(fileId)}) diff --git a/file.go b/file.go index 6021716..86ff522 100644 --- a/file.go +++ b/file.go @@ -27,7 +27,7 @@ var ( // The type implements the structure to easily send // files to the client. type File struct { - *MessageCompo + MessageCompo name string reader io.Reader upload bool @@ -108,7 +108,7 @@ func (f *File) SendData() string { func (f *File) SendConfig( sid SessionId, bot *Bot, -) (*SendConfig) { +) (SendConfig) { var config SendConfig cid := sid.ToApi() @@ -127,5 +127,5 @@ func (f *File) SendConfig( } - return &config + return config } diff --git a/go.go b/go.go new file mode 100644 index 0000000..985c290 --- /dev/null +++ b/go.go @@ -0,0 +1,22 @@ +package tg + +func Go(pth Path) UI { + return UI{ + GoWidget(pth), + } +} + +type GoWidget string +// Implementing the Server interface. +func (widget GoWidget) Serve(c Context) { + c.input.Close() + c.Go(Path(widget)) +} + +func (widget GoWidget) Render(c Context) UI { + return UI{widget} +} + +func (widget GoWidget) Filter(u Update) bool { + return true +} diff --git a/inline.go b/inline.go index 5047986..dc1a700 100644 --- a/inline.go +++ b/inline.go @@ -4,13 +4,14 @@ import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) -// The type represents keyboard to be emdedded into the messages (inline in Telegram terms). +// The type represents keyboard to be emdedded +// into the messages (inline in Telegram terms). type Inline struct { - *Keyboard + Keyboard } // Convert the inline keyboard to markup for the tgbotapi. -func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup { +func (kbd Inline) ToApi() tgbotapi.InlineKeyboardMarkup { rows := [][]tgbotapi.InlineKeyboardButton{} for _, row := range kbd.Rows { if row == nil { @@ -31,14 +32,14 @@ func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup { // The type implements message with an inline keyboard. type InlineCompo struct { - *MessageCompo - *Inline + MessageCompo + Inline } // Implementing the Sendable interface. func (compo *InlineCompo) SendConfig( sid SessionId, bot *Bot, -) (*SendConfig) { +) (SendConfig) { sendConfig := compo.MessageCompo.SendConfig(sid, bot) if len(compo.Inline.Rows) > 0 { sendConfig.Message.ReplyMarkup = compo.Inline.ToApi() @@ -48,7 +49,9 @@ func (compo *InlineCompo) SendConfig( } // Update the component on the client side. -func (compo *InlineCompo) Update(c *Context) { +// Requires exactly the pointer but not the value +// cause it changes insides of the structure. +func (compo *InlineCompo) Update(c Context) { if compo.Message != nil { var edit tgbotapi.Chattable markup := compo.Inline.ToApi() @@ -74,7 +77,7 @@ func (compo *InlineCompo) Update(c *Context) { } // Implementing the Filterer interface. -func (compo *InlineCompo) Filter(u *Update) bool { +func (compo InlineCompo) Filter(u Update) bool { if compo == nil || u.CallbackQuery == nil { return true } @@ -88,13 +91,13 @@ func (compo *InlineCompo) Filter(u *Update) bool { } // Implementing the Server interface. -func (compo *InlineCompo) Serve(c *Context) { +func (compo InlineCompo) Serve(c Context) { for u := range c.Input() { compo.OnOneUpdate(c, u) } } -func (compo *InlineCompo) OnOneUpdate(c *Context, u *Update) { +func (compo *InlineCompo) OnOneUpdate(c Context, u Update) { var act Action btns := compo.ButtonMap() cb := tgbotapi.NewCallback( diff --git a/keyboard.go b/keyboard.go index ac9c839..440cdc2 100644 --- a/keyboard.go +++ b/keyboard.go @@ -14,7 +14,7 @@ type Keyboard struct { } // Returns the new keyboard with specified rows. -func NewKeyboard(rows ...ButtonRow) *Keyboard { +func NewKeyboard(rows ...ButtonRow) Keyboard { ret := &Keyboard{} for _, row := range rows { if row != nil && len(row) > 0 { @@ -24,7 +24,7 @@ func NewKeyboard(rows ...ButtonRow) *Keyboard { return ret } -func (kbd *Keyboard) RowNum() int { +func (kbd Keyboard) RowNum() int { return len(kbd.Rows) } @@ -36,7 +36,7 @@ func (kbd *Keyboard) RemoveRow(i int) { } // Adds a new button row to the current keyboard. -func (kbd *Keyboard) Row(btns ...*Button) *Keyboard { +func (kbd Keyboard) Row(btns ...Button) Keyboard { // For empty row. We do not need that. if len(btns) < 1 { return kbd @@ -56,7 +56,7 @@ func (kbd *Keyboard) Row(btns ...*Button) *Keyboard { } // Adds buttons as one column list. -func (kbd *Keyboard) List(btns ...*Button) *Keyboard { +func (kbd Keyboard) List(btns ...Button) Keyboard { for _, btn := range btns { if btn == nil { continue @@ -68,18 +68,13 @@ func (kbd *Keyboard) List(btns ...*Button) *Keyboard { // Set the default action when no button provides // key to the data we got. -func (kbd *Keyboard) WithAction(a Action) *Keyboard { +func (kbd Keyboard) WithAction(a Action) Keyboard { kbd.Action = a return kbd } -// Alias to WithAction but better typing when setting -// a specific function -func (kbd *Keyboard) ActionFunc(fn ActionFunc) *Keyboard { - return kbd.WithAction(fn) -} - -// Returns the map of buttons. +// Returns the map of buttons. Where the key +// is button data and the value is Action. func (kbd Keyboard) ButtonMap() ButtonMap { if kbd.buttonMap == nil { kbd.buttonMap = kbd.MakeButtonMap() @@ -101,14 +96,14 @@ func (kbd Keyboard) MakeButtonMap() ButtonMap { } // Convert the keyboard to the more specific inline one. -func (kbd *Keyboard) Inline() *Inline { - ret := &Inline{} +func (kbd Keyboard) Inline() Inline { + ret := Inline{} ret.Keyboard = kbd return ret } // Convert the keyboard to the more specific reply one. -func (kbd *Keyboard) Reply() *Reply { +func (kbd Keyboard) Reply() Reply { ret := &Reply{} ret.Keyboard = kbd // it is used more often than not once. diff --git a/message.go b/message.go index 68f76ad..a6fe070 100644 --- a/message.go +++ b/message.go @@ -10,7 +10,7 @@ type Message = tgbotapi.Message // Simple text message component type. type MessageCompo struct { - Message *Message + Message Message ParseMode string Text string } @@ -26,14 +26,19 @@ func Escape2(str string) string { return string(escapeRe.ReplaceAll([]byte(str), []byte("\\$1"))) } -func (compo *MessageCompo) Update(c *Context) { +// Call the function after the message was sent. +func (compo *MessageCompo) Update(c Context) error { edit := tgbotapi.NewEditMessageText( c.Session.Id.ToApi(), compo.Message.MessageID, compo.Text, ) - msg, _ := c.Bot.Api.Send(edit) - compo.Message = &msg + msg, err := c.bot.api.Send(edit) + if err != nil { + return err + } + compo.Message = msg + return nil } func (compo *MessageCompo) Delete(c *Context) { @@ -44,59 +49,59 @@ func (compo *MessageCompo) Delete(c *Context) { // Is only implemented to make it sendable and so we can put it // return of rendering functions. -func (compo *MessageCompo) SetMessage(msg *Message) { +func (compo *MessageCompo) SetMessage(msg Message) { compo.Message = msg } // Return new message with the specified text. -func NewMessage(format string, v ...any) *MessageCompo { - ret := &MessageCompo{} +func Messagef(format string, v ...any) MessageCompo { + ret := MessageCompo{} ret.Text = fmt.Sprintf(format, v...) ret.ParseMode = tgbotapi.ModeMarkdown return ret } // Return message with the specified parse mode. -func (msg *MessageCompo) withParseMode(mode string) *MessageCompo { +func (msg MessageCompo) withParseMode(mode string) MessageCompo { msg.ParseMode = mode return msg } // Set the default Markdown parsing mode. -func (msg *MessageCompo) MD() *MessageCompo { +func (msg MessageCompo) MD() MessageCompo { return msg.withParseMode(tgbotapi.ModeMarkdown) } // Set the Markdown 2 parsing mode. -func (msg *MessageCompo) MD2() *MessageCompo { +func (msg MessageCompo) MD2() MessageCompo { return msg.withParseMode(tgbotapi.ModeMarkdownV2) } // Set the HTML parsing mode. -func (msg *MessageCompo) HTML() *MessageCompo { +func (msg MessageCompo) HTML() MessageCompo { return msg.withParseMode(tgbotapi.ModeHTML) } // Transform the message component into one with reply keyboard. -func (msg *MessageCompo) Inline(inline *Inline) *InlineCompo { - return &InlineCompo{ +func (msg MessageCompo) Inline(inline Inline) InlineCompo { + return InlineCompo{ Inline: inline, MessageCompo: msg, } } // Transform the message component into one with reply keyboard. -func (msg *MessageCompo) Reply(reply *Reply) *ReplyCompo { - return &ReplyCompo{ +func (msg MessageCompo) Reply(reply Reply) ReplyCompo { + return ReplyCompo{ Reply: reply, MessageCompo: msg, } } // Transform the message component into the location one. -func (msg *MessageCompo) Location( +func (msg MessageCompo) Location( lat, long float64, -) *LocationCompo { +) LocationCompo { ret := &LocationCompo{ MessageCompo: msg, Location: Location{ @@ -108,9 +113,9 @@ func (msg *MessageCompo) Location( } // Implementing the Sendable interface. -func (config *MessageCompo) SendConfig( +func (config MessageCompo) SendConfig( sid SessionId, bot *Bot, -) (*SendConfig) { +) (SendConfig) { var ( ret SendConfig text string @@ -122,20 +127,18 @@ func (config *MessageCompo) SendConfig( text = config.Text } - //text = strings.ReplaceAll(text, "-", "\\-") - msg := tgbotapi.NewMessage(sid.ToApi(), text) - ret.Message = &msg - ret.Message.ParseMode = config.ParseMode + msg.ParseMode = config.ParseMode + ret.Chattable = msg - return &ret + return ret } // Empty serving to use messages in rendering. -func (compo *MessageCompo) Serve(c *Context) {} +func (compo *MessageCompo) Serve(c Context) {} // Filter that skips everything. Messages cannot do anything with updates. -func (compo *MessageCompo) Filter(_ *Update) bool { +func (compo *MessageCompo) Filter(_ Update) bool { // Skip everything return true } diff --git a/reply.go b/reply.go index 06e8108..06a470a 100644 --- a/reply.go +++ b/reply.go @@ -6,7 +6,7 @@ import ( // The type represents reply keyboards. type Reply struct { - *Keyboard + Keyboard // If true will be removed after one press. OneTime bool // If true will remove the keyboard on send. @@ -15,20 +15,20 @@ type Reply struct { // Set if we should remove current keyboard on the user side // when sending the keyboard. -func (kbd *Reply) WithRemove(remove bool) *Reply { +func (kbd Reply) WithRemove(remove bool) Reply { kbd.Remove = remove return kbd } // Set if the keyboard should be hidden after // one of buttons is pressede. -func (kbd *Reply) WithOneTime(oneTime bool) *Reply{ +func (kbd Reply) WithOneTime(oneTime bool) Reply{ kbd.OneTime = oneTime return kbd } // Convert the Keyboard to the Telegram API type of reply keyboard. -func (kbd *Reply) ToApi() any { +func (kbd Reply) ToApi() any { // Shades everything. if kbd.Remove { return tgbotapi.NewRemoveKeyboard(true) @@ -58,20 +58,21 @@ func (kbd *Reply) ToApi() any { // The type implements reply keyboard widget. type ReplyCompo struct { - *MessageCompo - *Reply + MessageCompo + Reply } // Implementing the sendable interface. -func (compo *ReplyCompo) SendConfig( +func (compo ReplyCompo) SendConfig( sid SessionId, bot *Bot, -) (*SendConfig) { +) (SendConfig) { sendConfig := compo.MessageCompo.SendConfig(sid, bot) sendConfig.Message.ReplyMarkup = compo.Reply.ToApi() return sendConfig } -func (compo *ReplyCompo) Filter( +// Implementing the Server interface. +func (compo ReplyCompo) Filter( u *Update, ) bool { if compo == nil || u.Message == nil { @@ -93,7 +94,7 @@ func (compo *ReplyCompo) Filter( } // Implementing the UI interface. -func (compo *ReplyCompo) Serve(c *Context) { +func (compo ReplyCompo) Serve(c *Context) { for u := range c.Input() { var btn *Button text := u.Message.Text diff --git a/send.go b/send.go index 7f2ae54..48cdc5d 100644 --- a/send.go +++ b/send.go @@ -10,24 +10,14 @@ type MessageId int64 // way to define what message will be // sent to the side of a user. type Sendable interface { - SendConfig(SessionId, *Bot) (*SendConfig) - SetMessage(*Message) -} - -type Errorer interface { - Err() error + SendConfig(SessionId, *Bot) (SendConfig) + SetMessage(Message) } // The type is used as an endpoint to send messages // via bot.Send . type SendConfig struct { - // Message with text and keyboard. - Message *tgbotapi.MessageConfig - - // The image to be sent. - Photo *tgbotapi.PhotoConfig - Document *tgbotapi.DocumentConfig - Location *tgbotapi.LocationConfig + Chattable tgbotapi.Chattable Error error } @@ -35,17 +25,7 @@ type SendConfig struct { type MessageMap map[string] *Message // Convert to the bot.Api.Send format. -func (config *SendConfig) ToApi() tgbotapi.Chattable { - switch { - case config.Message != nil : - return *(config.Message) - case config.Photo != nil : - return *(config.Photo) - case config.Location != nil : - return *(config.Location) - case config.Document != nil : - return *(config.Document) - } - return nil +func (config SendConfig) ToApi() tgbotapi.Chattable { + return config.Chattable } diff --git a/session.go b/session.go index 5d5709a..1c554f5 100644 --- a/session.go +++ b/session.go @@ -1,5 +1,16 @@ package tg +// The type represents map of sessions using +// as key. +type SessionMap map[SessionId]*Session + +// Add new empty session by it's ID. +func (sm SessionMap) Add(sid SessionId, scope SessionScope) *Session { + ret := NewSession(sid, scope) + sm[sid] = ret + return ret +} + // The way to determine where the context is // related to. type SessionScope uint8 @@ -27,6 +38,11 @@ type Session struct { Scope SessionScope // Custom value for each user. Data any + + bot *Bot + pathHistory []Path + skippedUpdates *UpdateChan + updates *UpdateChan } // Return new empty session with specified user ID. @@ -37,14 +53,129 @@ func NewSession(id SessionId, scope SessionScope) *Session { } } -// The type represents map of sessions using -// as key. -type SessionMap map[SessionId]*Session +// Changes screen of user to the Id one. +func (s *Session) go_(pth Path, arg any) error { + var err error + if pth == "" { + s.pathHistory = []Path{} + return nil + } + var back bool + if pth == "-" { + if len(s.pathHistory) < 2 { + return s.Go("") + } + pth = s.pathHistory[len(s.pathHistory)-2] + s.pathHistory = s.pathHistory[:len(s.pathHistory)-1] + } + // Getting the screen and changing to + // then executing its widget. + if !pth.IsAbs() { + pth = (s.Path() + "/" + pth).Clean() + } -// Add new empty session by it's ID. -func (sm SessionMap) Add(sid SessionId, scope SessionScope) *Session { - ret := NewSession(sid, scope) - sm[sid] = ret - return ret + if !s.PathExist(pth) { + return ScreenNotExistErr + } + + if !back && s.Path() != pth { + s.pathHistory = append(s.pathHistory, pth) + } + + // Stopping the current widget. + screen := s.bot.behaviour.Screens[pth] + s.skippedUpdates.Close() + + if screen.Widget != nil { + s.skippedUpdates, err = s.runWidget(screen.Widget, arg) + if err != nil { + return err + } + } else { + return NoWidgetForScreenErr + } + + return nil } +func (s *Session) runCompo(compo Component, arg any) (*UpdateChan, error) { + if compo == nil { + return nil, nil + } + s, ok := compo.(Sendable) + if ok { + msg, err := c.Send(s) + if err != nil { + return nil, err + } + s.SetMessage(msg) + } + updates := NewUpdateChan() + go func() { + compo.Serve( + c.WithInput(updates). + WithArg(arg), + ) + // To let widgets finish themselves before + // the channel is closed and close it by themselves. + updates.Close() + }() + return updates, nil +} + +// Run widget in background returning the new input channel for it. +func (c *Context) runWidget(widget Widget, arg any) (*UpdateChan, error) { + var err error + if widget == nil { + return nil, EmptyWidgetErr + } + + pth := c.Path() + compos := widget.Render(c.WithArg(c.makeArg(args))) + // Leave if changed path or components are empty. + if compos == nil || pth != c.Path() { + return nil, EmptyCompoErr + } + chns := make([]*UpdateChan, len(compos)) + for i, compo := range compos { + chns[i], err = c.runCompo(compo, arg) + if err != nil { + for _, chn := range chns { + chn.Close() + } + return nil, err + } + } + + ret := NewUpdateChan() + go func() { + ln := len(compos) + UPDATE: + for u := range ret.Chan() { + if u == nil { + break + } + cnt := 0 + for i, compo := range compos { + chn := chns[i] + if chn.Closed() { + cnt++ + continue + } + if !compo.Filter(u) { + chn.Send(u) + continue UPDATE + } + } + if cnt == ln { + break + } + } + ret.Close() + for _, chn := range chns { + chn.Close() + } + }() + + return ret, nil +} diff --git a/update.go b/update.go index 7932745..219ddd2 100644 --- a/update.go +++ b/update.go @@ -5,13 +5,12 @@ import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" type FileId string type Update struct { - *tgbotapi.Update - c *Context + tgbotapi.Update } // The type represents general update channel. type UpdateChan struct { - chn chan *Update + chn chan Update } // Return new update channel. @@ -60,13 +59,13 @@ func (updates *UpdateChan) Close() { close(chn) } -func (u *Update) HasDocument() bool { +func (u Update) HasDocument() bool { return u != nil && u.Message != nil && u.Message.Document != nil } -func (u *Update) DocumentId() FileId { +func (u Update) DocumentId() FileId { return FileId(u.Update.Message.Document.FileID) } @@ -74,23 +73,24 @@ func (u *Update) DocumentName() string { return u.Message.Document.FileName } -func (u *Update) DocumentSize() int { +func (u Update) DocumentSize() int { return u.Message.Document.FileSize } -func (u *Update) DocumentMimeType() string { +func (u Update) DocumentMimeType() string { return u.Message.Document.MimeType } -func (u *Update) HasPhotos() bool { +func (u Update) HasPhotos() bool { return u.Message != nil && u.Message.Photo != nil && len(u.Message.Photo) != 0 } -func (u *Update) PhotoIds() []FileId { +func (u Update) PhotoIds() []FileId { ret := make([]FileId, len(u.Message.Photo)) for i, photo := range u.Message.Photo { ret[i] = FileId(photo.FileID) } return ret } +