410 lines
7.9 KiB
Go
410 lines
7.9 KiB
Go
package tg
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
//"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
|
|
// 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
|
|
}
|
|
|
|
// 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 nil if the user is not logged in.
|
|
func (c *Context) As(sid SessionId) *Context {
|
|
n, ok := c.Bot.contexts[sid]
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return &Context{
|
|
context: n,
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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,
|
|
}
|
|
}
|
|
|
|
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)
|
|
beh.Root.Serve(c)
|
|
}
|
|
|
|
func (c *Context) Path() Path {
|
|
ln := len(c.pathHistory)
|
|
if ln == 0 {
|
|
return ""
|
|
}
|
|
return c.pathHistory[ln-1]
|
|
}
|
|
|
|
func (c *Context) Arg() any {
|
|
return c.arg
|
|
}
|
|
|
|
func (c *Context) Run(a Action) {
|
|
if a != nil {
|
|
a.Act(c)
|
|
}
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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(NewMessage(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(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) {
|
|
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())
|
|
}
|
|
|
|
// 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 = c.Copy()
|
|
c.arg = v
|
|
return c
|
|
}
|
|
|
|
func (c *Context) WithUpdate(u *Update) *Context {
|
|
c = c.Copy()
|
|
c.Update = u
|
|
return c
|
|
}
|
|
|
|
func (c *Context) WithInput(input *UpdateChan) *Context {
|
|
c = c.Copy()
|
|
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
|
|
}
|
|
|
|
// Changes screen of user to the Id one.
|
|
func (c *Context) Go(pth Path, args ...any) {
|
|
if pth == "" {
|
|
c.pathHistory = []Path{}
|
|
return
|
|
}
|
|
var back bool
|
|
if pth == "-" {
|
|
ln := len(c.pathHistory)
|
|
if ln <= 1 {
|
|
pth = "/"
|
|
} else {
|
|
pth = c.pathHistory[ln-2]
|
|
c.pathHistory = c.pathHistory[:ln-1]
|
|
back = true
|
|
}
|
|
}
|
|
// Getting the screen and changing to
|
|
// then executing its widget.
|
|
if !pth.IsAbs() {
|
|
pth = (c.Path() + "/" + pth).Clean()
|
|
}
|
|
|
|
if !c.PathExist(pth) {
|
|
panic(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 = 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 {
|
|
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
|
|
}
|
|
|
|
pth := c.Path()
|
|
compos := widget.Render(c.WithArg(c.makeArg(args)))
|
|
// Leave if changed path.
|
|
if compos == nil || pth != c.Path() {
|
|
return nil
|
|
}
|
|
chns := make([]*UpdateChan, len(compos))
|
|
for i, compo := range compos {
|
|
chns[i] = c.RunCompo(compo, args...)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// Simple way to read strings for widgets.
|
|
func (c *Context) ReadString(pref string, args ...any) string {
|
|
var text string
|
|
if pref != "" {
|
|
c.Sendf(pref, args...)
|
|
}
|
|
for u := range c.Input() {
|
|
if u == nil {
|
|
break
|
|
}
|
|
if u.Message == nil {
|
|
continue
|
|
}
|
|
text = u.Message.Text
|
|
break
|
|
}
|
|
return text
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|