tg/context.go

387 lines
7.8 KiB
Go
Raw Normal View History

2023-08-19 09:12:26 +03:00
package tg
2023-09-25 19:58:59 +03:00
import (
"fmt"
"io"
"net/http"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
2023-09-25 19:58:59 +03:00
//"path"
)
// Interface to interact with the user.
type Context struct {
2024-03-29 14:30:48 +03:00
// 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
2024-03-29 14:30:48 +03:00
// or false if the user is not logged in.
func (c Context) As(sid SessionID) (Context, bool) {
2024-03-29 14:30:48 +03:00
s, ok := c.Bot().GotSession(sid)
if !ok {
2024-03-29 14:30:48 +03:00
return Context{}, false
}
2024-03-29 14:30:48 +03:00
c.session = s
return c, true
}
2023-09-26 17:13:31 +03:00
// 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)
}
2024-03-29 14:30:48 +03:00
func(f Func) Filter(_ Update) bool {
2023-09-26 17:13:31 +03:00
return false
}
2024-03-29 14:30:48 +03:00
func (f Func) Render(_ Context) UI {
2023-09-26 17:13:31 +03:00
return UI{
f,
}
}
2023-09-25 19:58:59 +03:00
// The type represents type
// of current context the processing is happening
// in.
2023-09-25 19:58:59 +03:00
type ContextType uint8
const (
NoContextType ContextType = iota
WidgetContextType
ActionContextType
)
// Goroutie function to handle each user.
func (c Context) serve() {
2024-03-29 14:30:48 +03:00
beh := c.Bot().behaviour
2023-09-26 17:13:31 +03:00
c.Run(beh.Init)
for {
defer func(){
if err := recover() ; err != nil {
// Need to add some handling later.
}
}()
beh.Root.Serve(c)
}
2023-09-25 19:58:59 +03:00
}
func (c Context) Arg() any {
return c.arg
}
func (c Context) Run(a Action) {
2023-09-25 19:58:59 +03:00
if a != nil {
a.Act(c)
2023-09-25 19:58:59 +03:00
}
}
2024-03-29 14:30:48 +03:00
// 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
2023-09-25 19:58:59 +03:00
}
// Sends the formatted with fmt.Sprintf message to the user
// using default Markdown parsing format.
2024-03-29 14:30:48 +03:00
func (c Context) Sendf(format string, v ...any) (*Message, error) {
return c.Send(Messagef(format, v...))
2023-09-25 19:58:59 +03:00
}
// Same as Sendf but uses Markdown 2 format for parsing.
2024-03-29 14:30:48 +03:00
func (c Context) Sendf2(format string, v ...any) (*Message, error) {
return c.Send(Messagef(format, v...).MD2())
2023-09-25 19:58:59 +03:00
}
// Same as Sendf but uses HTML format for parsing.
2024-03-29 14:30:48 +03:00
func (c Context) SendfHTML(format string, v ...any) (*Message, error) {
return c.Send(Messagef(format, v...).HTML())
2023-09-25 19:58:59 +03:00
}
// Send the message in raw format escaping all the special characters.
2024-03-29 14:30:48 +03:00
func (c Context) SendfR(format string, v ...any) (*Message, error) {
return c.Send(Messagef("%s", Escape2(fmt.Sprintf(format, v...))).MD2())
}
2023-09-25 19:58:59 +03:00
// Get the input for current widget.
// Should be used inside handlers (aka "Serve").
func (c Context) Input() chan Update {
2023-09-25 19:58:59 +03:00
return c.input.Chan()
}
func (c Context) WithArg(v any) Context {
c.arg = v
2023-09-25 19:58:59 +03:00
return c
}
2024-03-29 14:30:48 +03:00
func (c Context) WithUpdate(u Update) Context {
c.update = u
2023-09-25 19:58:59 +03:00
return c
}
func (c Context) WithInput(input *UpdateChan) Context {
2023-09-25 19:58:59 +03:00
c.input = input
return c
}
// Customized actions for the bot.
type Action interface {
Act(Context)
2023-09-25 19:58:59 +03:00
}
type ActionFunc func(Context)
2023-09-25 19:58:59 +03:00
2024-03-29 14:30:48 +03:00
func (af ActionFunc) Act(c Context) {
2023-09-25 19:58:59 +03:00
af(c)
}
// Simple way to read strings for widgets with
// the specified prompt.
func (c Context) ReadString(promptf string, args ...any) string {
2023-09-25 19:58:59 +03:00
var text string
2024-03-29 14:30:48 +03:00
if promptf != "" {
c.Sendf(promptf, args...)
2023-10-02 21:45:21 +03:00
}
2023-09-25 19:58:59 +03:00
for u := range c.Input() {
if u.Message == nil {
continue
}
text = u.Message.Text
break
}
return text
}
2024-03-29 14:30:48 +03:00
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
}
2024-03-29 14:30:48 +03:00
// 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
}
2024-03-29 14:30:48 +03:00
func (c Context) runCompo(compo Component, arg any) (*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).
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
}
compos := widget.Render(c.WithArg(arg))
// Leave if changed path or components are empty.
if compos == nil {
2024-03-29 14:30:48 +03:00
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)
//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
}
// Simple go without an argument for context.
func (c Context) Go(pth Widget) error {
2024-03-29 14:30:48 +03:00
return c.GoWithArg(pth, nil)
}
// Go to the specified widget with the
// specificed argument.
func (c Context) GoWithArg(pth Widget, arg any) error {
2024-03-29 14:30:48 +03:00
var err error
if pth == nil {
c.session.pathHistory = []Widget{}
2024-03-29 14:30:48 +03:00
return nil
}
2024-03-29 14:30:48 +03:00
var back bool
if pth == Back {
2024-03-29 14:30:48 +03:00
if len(c.session.pathHistory) < 2 {
return c.GoWithArg(nil, arg)
2024-03-29 14:30:48 +03:00
}
pth = c.session.pathHistory[len(c.session.pathHistory)-2]
c.session.pathHistory =
c.session.pathHistory[:len(c.session.pathHistory)-1]
back = true
2024-03-29 14:30:48 +03:00
}
if !back {
2024-03-29 14:30:48 +03:00
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, arg)
if err != nil {
return err
2024-03-29 14:30:48 +03:00
}
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
2024-03-29 14:30:48 +03:00
}
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 {
2024-03-29 14:30:48 +03:00
return c.session.pathHistory
}
func (c Context) Path() Widget {
2024-03-29 14:30:48 +03:00
ln := len(c.session.pathHistory)
if ln == 0 {
return nil
2024-03-29 14:30:48 +03:00
}
return c.session.pathHistory[ln-1]
}