tg/context.go

383 lines
7.3 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"
)
2023-12-12 19:32:30 +03:00
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
}
// Interface to interact with the user.
type Context struct {
*context
// The update that called the Context usage.
*Update
// Used as way to provide outer values redirection
// into widgets and actions. It is like arguments
// for REST API request etc.
arg any
// Instead of updates as argument.
input *UpdateChan
}
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)
}
func(f Func) Filter(_ *Update) bool {
2023-09-26 17:13:31 +03:00
return false
}
func (f Func) Render(_ *Context) UI {
return UI{
f,
}
}
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() {
beh := c.Bot.behaviour
2023-09-26 17:13:31 +03:00
c.Run(beh.Init)
2023-09-25 19:58:59 +03:00
beh.Root.Serve(c)
}
func (c *Context) Path() Path {
ln := len(c.pathHistory)
if ln == 0 {
return ""
}
return c.pathHistory[ln-1]
2023-09-25 19:58:59 +03:00
}
func (c *Context) Arg() any {
return c.arg
}
2023-09-26 17:13:31 +03:00
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
}
}
// Only for the root widget usage.
// Skip the update sending it down to
// the underlying widget.
func (c *Context) Skip(u *Update) {
c.skippedUpdates.Send(u)
}
// Sends to the Sendable object.
func (c *Context) Send(v Sendable) (*Message, error) {
config := v.SendConfig(c.Session.Id, 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.
2023-09-25 19:58:59 +03:00
func (c *Context) Sendf(format string, v ...any) (*Message, error) {
return c.Send(NewMessage(fmt.Sprintf(format, v...)))
}
// Same as Sendf but uses Markdown 2 format for parsing.
2023-09-25 19:58:59 +03:00
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.
2023-09-25 19:58:59 +03:00
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) {
return c.Send(NewMessage(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 {
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 {
2023-09-26 17:13:31 +03:00
c = c.Copy()
c.arg = v
2023-09-25 19:58:59 +03:00
return c
}
func (c *Context) WithUpdate(u *Update) *Context {
2023-09-26 17:13:31 +03:00
c = c.Copy()
2023-09-25 19:58:59 +03:00
c.Update = u
return c
}
func (c *Context) WithInput(input *UpdateChan) *Context {
2023-09-26 17:13:31 +03:00
c = c.Copy()
2023-09-25 19:58:59 +03:00
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)
}
func (c *Context) History() []Path {
return c.pathHistory
}
2023-09-25 19:58:59 +03:00
// Changes screen of user to the Id one.
func (c *Context) Go(pth Path, args ...any) {
var back bool
2023-10-02 21:45:21 +03:00
if pth == "-" {
ln := len(c.pathHistory)
if ln <= 1 {
pth = "/"
} else {
pth = c.pathHistory[ln-2]
c.pathHistory = c.pathHistory[:ln-1]
back = true
}
2023-10-02 21:45:21 +03:00
}
2023-09-25 19:58:59 +03:00
// Getting the screen and changing to
// then executing its widget.
if !pth.IsAbs() {
pth = (c.Path() + "/" + pth).Clean()
}
if !c.PathExist(pth) {
panic(ScreenNotExistErr)
2023-09-25 19:58:59 +03:00
}
if !back && c.Path() != pth {
c.pathHistory = append(c.pathHistory, pth)
}
2023-09-25 19:58:59 +03:00
// Stopping the current widget.
screen := c.Bot.behaviour.Screens[pth]
c.skippedUpdates.Close()
if screen.Widget != nil {
c.skippedUpdates = c.RunWidget(screen.Widget, args...)
} else {
panic("no widget defined for the screen")
}
}
func (c *Context) PathExist(pth Path) bool {
return c.Bot.behaviour.PathExist(pth)
}
func (c *Context) makeArg(args []any) any {
2023-09-25 19:58:59 +03:00
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 {
if compo == nil {
return nil
}
s, ok := compo.(Sendable)
if ok {
msg, err := c.Send(s)
if err != nil {
panic("could not send the message")
}
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
}
// 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
}
2023-09-25 19:58:59 +03:00
2023-09-26 17:13:31 +03:00
pth := c.Path()
compos := widget.Render(c.WithArg(c.makeArg(args)))
2023-09-26 17:13:31 +03:00
// Leave if changed path.
if compos == nil || pth != c.Path() {
2023-09-26 17:13:31 +03:00
return nil
}
chns := make([]*UpdateChan, len(compos))
for i, compo := range compos {
2023-10-02 21:45:21 +03:00
chns[i] = c.RunCompo(compo, args...)
}
2023-09-26 17:13:31 +03:00
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
2023-09-26 17:13:31 +03:00
}
}
if cnt == ln {
break
}
}
2023-09-26 17:13:31 +03:00
ret.Close()
for _, chn := range chns {
chn.Close()
}
}()
2023-09-26 17:13:31 +03:00
return ret
2023-09-25 19:58:59 +03:00
}
// Simple way to read strings for widgets.
func (c *Context) ReadString(pref string, args ...any) string {
var text string
2023-10-02 21:45:21 +03:00
if pref != "" {
c.Sendf(pref, args...)
}
2023-09-25 19:58:59 +03:00
for u := range c.Input() {
2023-10-02 21:45:21 +03:00
if u == nil {
break
}
2023-09-25 19:58:59 +03:00
if u.Message == nil {
continue
}
text = u.Message.Text
break
}
return text
}
func (c *Context) GetFile(fileId FileId) (io.ReadCloser, 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, nil
}
func (c *Context) ReadFile(fileId FileId) ([]byte, error) {
file, 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, nil
}