package tg import ( "fmt" "io" "net/http" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" //"path" //"log" ) // Interface to interact with the user. type Context struct { // The session contains all // the information between the contexts. session *Session // The update that called the Context usage. 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 } // Run commands as other user. Was implemented to // make other user to leave the bot at first but // maybe you will find another usage for this. // Returns users context by specified session ID // or false if the user is not logged in. func (c Context) As(sid SessionID) (Context, bool) { s, ok := c.Bot().GotSession(sid) if !ok { return Context{}, false } c.session = s return c, true } // General type function to define actions, single component widgets // and components themselves. type Func func(Context) func (f Func) Act(c Context) { f(c) } func (f Func) Serve(c Context) { f(c) } func(f Func) Filter(_ Update) bool { return false } func (f Func) Render(_ Context) UI { return UI{ f, } } // The type represents type // of current context the processing is happening // in. type ContextType uint8 const ( NoContextType ContextType = iota WidgetContextType ActionContextType ) // Goroutie function to handle each user. func (c Context) serve() { beh := c.Bot().behaviour c.Run(beh.Init) for { defer func(){ if err := recover() ; err != nil { // Need to add some handling later. } }() beh.Root.Serve(c) } } func (c Context) Arg() any { return c.arg } func (c Context) Run(a Action) { if a != nil { a.Act(c) } } // Sends to the Sendable object to the session user. func (c Context) Send(v Sendable) (*Message, error) { config := v.SendConfig(c.SessionID(), c.Bot()) if config.Error != nil { return nil, config.Error } msg, err := c.Bot().API().Send(config.ToAPI()) if err != nil { return nil, err } return &msg, nil } // 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) { return c.Send(Messagef(format, v...)) } // Same as Sendf but uses Markdown 2 format for parsing. func (c Context) Sendf2(format string, v ...any) (*Message, error) { return c.Send(Messagef(format, v...).MD2()) } // Same as Sendf but uses HTML format for parsing. func (c Context) SendfHTML(format string, v ...any) (*Message, error) { return c.Send(Messagef(format, v...).HTML()) } // Send the message in raw format escaping all the special characters. func (c Context) SendfR(format string, v ...any) (*Message, error) { return c.Send(Messagef("%s", 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 { return c.input.Chan() } 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) } type ActionFunc func(Context) func (af ActionFunc) Act(c Context) { af(c) } // Simple way to read strings for widgets with // the specified prompt. func (c Context) ReadString(promptf string, args ...any) string { var text string if promptf != "" { c.Sendf(promptf, args...) } for u := range c.Input() { if u.Message == nil { continue } text = u.Message.Text break } return text } func (c Context) CallbackUpdate() *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), }) if err != nil { return nil, "", err } r, err := http.Get(fmt.Sprintf( "https://api.telegram.org/file/bot%s/%s", c.Bot().API().Token, file.FilePath, )) if err != nil { return nil, "", err } if r.StatusCode != 200 { return nil, "", StatusCodeErr } return r.Body, file.FilePath, nil } // Reads all the content from the specified file. func (c Context) ReadFile(fileID FileID) ([]byte, string, error) { file, pth, err := c.GetFile(fileID) if err != nil { return nil, "", err } defer file.Close() bts, err := io.ReadAll(file) if err != nil { return nil, "", err } return bts, pth, nil } func (c Context) RunCompo(compo Component) (*UpdateChan, error) { if compo == nil { return nil, nil } sendable, canSend := compo.(Sendable) if canSend { msg, err := c.Send(sendable) if err != nil { return nil, err } sendable.SetMessage(msg) } updates := NewUpdateChan() go func() { compo.Serve( c.WithInput(updates), ) // 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) (*UpdateChan, error) { var err error if widget == nil { return nil, EmptyWidgetErr } compos := widget.Render(c) // Leave if changed path or components are empty. if compos == nil { return nil, EmptyCompoErr } chns := make([]*UpdateChan, len(compos)) for i, compo := range compos { chns[i], err = c.RunCompo(compo) if err != nil { for _, chn := range chns { chn.Close() } return nil, err } } ret := NewUpdateChan() go func() { ln := len(compos) //ation: u != nil (mismatchedtypes Update and untyped nil) UPDATE: for u := range ret.Chan() { 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 } func (c Context) GoRet(pth Widget) UI { return UI{WidgetGo{ Path: pth, Arg: c.Arg(), }} } // Go to the specified widget // using context values. func (c Context) Go(pth Widget) error { var err error if pth == nil { c.session.pathHistory = []Widget{} return nil } var back bool if pth == Back { if len(c.session.pathHistory) <= 1 { return c.Go(nil) } pth = c.session.pathHistory[len(c.session.pathHistory)-2] c.session.pathHistory = c.session.pathHistory[:len(c.session.pathHistory)-1] back = true } if !back { c.session.pathHistory = append(c.session.pathHistory, pth) } // Stopping the current widget. c.session.skippedUpdates.Close() // Running the new one. c.session.skippedUpdates, err = c.RunWidget(pth) if err != nil { return err } return nil } func (c Context) Session() Session { return *c.session } func (c Context) SetSessionData(v any) { c.session.Data = v } func (c Context) SessionData() any { return c.session.Data } func (c Context) SessionID() SessionID { return c.session.ID } func (c Context) SessionScope() SessionScope { return c.session.Scope } // Only for the root widget usage. // Skip the update sending it down to // the underlying widget. func (c Context) SkipUpdate(u Update) { c.session.skippedUpdates.Send(u) } // Return the session related bot. func (c Context) Bot() *Bot { return c.session.bot } // Return context's session's path history. func (c Context) PathHistory() []Widget { return c.session.pathHistory } func (c Context) Path() Widget { ln := len(c.session.pathHistory) if ln == 0 { return nil } return c.session.pathHistory[ln-1] }