feat: another refactoring. Less code needed. No focus on marshalling now.

This commit is contained in:
Andrey Parhomenko 2024-07-21 18:02:47 +05:00
parent c576e891b8
commit 1471d7cdae
26 changed files with 205 additions and 260 deletions

13
beh.go
View file

@ -4,13 +4,12 @@ package tg
type Behaviour struct { type Behaviour struct {
Root Component Root Component
Init Action Init Action
Screens ScreenMap //Screens ScreenMap
} }
// Returns new empty behaviour. // Returns new empty behaviour.
func NewBehaviour() *Behaviour { func NewBehaviour() *Behaviour {
return &Behaviour{ return &Behaviour{
Screens: make(ScreenMap),
} }
} }
@ -21,6 +20,11 @@ func (b *Behaviour) SetInit(a Action) *Behaviour {
return b return b
} }
/*func (b *Behaviour) SetScreens(screens ScreenMap) *Behaviour {
b.Screens = screens
return b
}
// Sets the root node of the Behaviour. // Sets the root node of the Behaviour.
// Mostly used for commands and such stuff. // Mostly used for commands and such stuff.
func (b *Behaviour) SetRootNode(node *RootNode) *Behaviour { func (b *Behaviour) SetRootNode(node *RootNode) *Behaviour {
@ -28,6 +32,7 @@ func (b *Behaviour) SetRootNode(node *RootNode) *Behaviour {
return b return b
} }
*/
// The function sets as the standard root widget CommandWidget // The function sets as the standard root widget CommandWidget
// and its commands.. // and its commands..
func (b *Behaviour) SetRootWidget(root Component) *Behaviour { func (b *Behaviour) SetRootWidget(root Component) *Behaviour {
@ -35,6 +40,7 @@ func (b *Behaviour) SetRootWidget(root Component) *Behaviour {
return b return b
} }
/*
// Check whether the screen exists in the behaviour. // Check whether the screen exists in the behaviour.
func (beh *Behaviour) PathExist(pth Path) bool { func (beh *Behaviour) PathExist(pth Path) bool {
_, ok := beh.Screens[pth] _, ok := beh.Screens[pth]
@ -43,7 +49,6 @@ func (beh *Behaviour) PathExist(pth Path) bool {
// Returns the screen by it's ID. // Returns the screen by it's ID.
func (beh *Behaviour) GetScreen(pth Path) *Screen { func (beh *Behaviour) GetScreen(pth Path) *Screen {
pth = pth.Clean()
if !beh.PathExist(pth) { if !beh.PathExist(pth) {
panic(ScreenNotExistErr) panic(ScreenNotExistErr)
} }
@ -51,4 +56,4 @@ func (beh *Behaviour) GetScreen(pth Path) *Screen {
screen := beh.Screens[pth] screen := beh.Screens[pth]
return screen return screen
} }
*/

23
bot.go
View file

@ -43,7 +43,7 @@ func (bot *Bot) SetDebug(debug bool) *Bot {
return bot return bot
} }
func (bot *Bot) Api() *tgbotapi.BotAPI { func (bot *Bot) API() *tgbotapi.BotAPI {
return bot.api return bot.api
} }
@ -53,16 +53,16 @@ func (bot *Bot) Me() User {
// Send the Renderable to the specified session client side. // Send the Renderable to the specified session client side.
// Can be used for both group and private sessions because // Can be used for both group and private sessions because
// SessionId represents both for chat IDs. // SessionID represents both for chat IDs.
func (bot *Bot) Send( func (bot *Bot) Send(
sid SessionId, v Sendable, sid SessionID, v Sendable,
) (*Message, error) { ) (*Message, error) {
config := v.SendConfig(sid, bot) config := v.SendConfig(sid, bot)
if config.Error != nil { if config.Error != nil {
return nil, config.Error return nil, config.Error
} }
msg, err := bot.api.Send(config.ToApi()) msg, err := bot.api.Send(config.ToAPI())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -71,7 +71,7 @@ func (bot *Bot) Send(
} }
func (bot *Bot) Sendf( func (bot *Bot) Sendf(
sid SessionId, format string, v ...any, sid SessionID, format string, v ...any,
) (*Message, error){ ) (*Message, error){
return bot.Send( return bot.Send(
sid, sid,
@ -81,7 +81,7 @@ func (bot *Bot) Sendf(
// Send to the session specified its ID raw chattable from the tgbotapi. // Send to the session specified its ID raw chattable from the tgbotapi.
func (bot *Bot) SendRaw( func (bot *Bot) SendRaw(
sid SessionId, v tgbotapi.Chattable, sid SessionID, v tgbotapi.Chattable,
) (*Message, error) { ) (*Message, error) {
msg, err := bot.api.Send(v) msg, err := bot.api.Send(v)
if err != nil { if err != nil {
@ -93,7 +93,7 @@ func (bot *Bot) SendRaw(
// Get session by its ID. Can be used for any scope // Get session by its ID. Can be used for any scope
// including private, group and channel. // including private, group and channel.
func (bot *Bot) GotSession( func (bot *Bot) GotSession(
sid SessionId, sid SessionID,
) (*Session, bool) { ) (*Session, bool) {
session, ok := bot.sessions[sid] session, ok := bot.sessions[sid]
return session, ok return session, ok
@ -158,7 +158,7 @@ func (bot *Bot) SetCommands(
botCmds := []tgbotapi.BotCommand{} botCmds := []tgbotapi.BotCommand{}
for _, cmd := range cmds { for _, cmd := range cmds {
botCmds = append(botCmds, cmd.ToApi()) botCmds = append(botCmds, cmd.ToAPI())
} }
//tgbotapi.NewBotCommandScopeAllPrivateChats(), //tgbotapi.NewBotCommandScopeAllPrivateChats(),
@ -210,7 +210,7 @@ func (bot *Bot) Run() error {
go bot.handleGroup(chn) go bot.handleGroup(chn)
}*/ }*/
me, _ := bot.Api().GetMe() me, _ := bot.API().GetMe()
bot.me = me bot.me = me
for up := range updates { for up := range updates {
u := Update{ u := Update{
@ -238,11 +238,10 @@ func (bot *Bot) Run() error {
// The function handles updates supposed for the private // The function handles updates supposed for the private
// chat with the bot. // chat with the bot.
func (bot *Bot) handlePrivate(updates chan Update) { func (bot *Bot) handlePrivate(updates chan Update) {
var sid SessionId var sid SessionID
for u := range updates { for u := range updates {
sid = SessionId(u.FromChat().ID) sid = SessionID(u.FromChat().ID)
session, sessionOk := bot.sessions[sid] session, sessionOk := bot.sessions[sid]
if u.Message != nil && !sessionOk { if u.Message != nil && !sessionOk {
// Creating session if we have none // Creating session if we have none
// but only on text messages. // but only on text messages.

View file

View file

@ -78,14 +78,14 @@ func (btn Button) WithSendLocation(ok bool) Button {
return btn return btn
} }
func (btn Button) Go(pth Path) Button { func (btn Button) Go(pth Widget) Button {
return btn.WithAction(ScreenGo{ return btn.WithAction(WidgetGo{
Path: pth, Path: pth,
}) })
} }
func (btn Button) GoWithArg(pth Path, arg any) Button { func (btn Button) GoWithArg(pth Widget, arg any) Button {
return btn.WithAction(ScreenGo{ return btn.WithAction(WidgetGo{
Path: pth, Path: pth,
Arg: arg, Arg: arg,
}) })

View file

@ -20,7 +20,7 @@ var BotCommands = []tg.Command{
tg.NewCommand( tg.NewCommand(
"start", "start",
"start or restart the bot or move to the start screen", "start or restart the bot or move to the start screen",
).Go(StartAbsPath), ).Go(StartWidget),
tg.NewCommand( tg.NewCommand(
"info", "info",
"info desc", "info desc",

View file

@ -5,10 +5,9 @@ import (
"fmt" "fmt"
) )
const ( // A simple example widget to show
IncDecPath tg.Path = "inc-dec" // how to store and get session data values
) // and working with dynamic panels.
var IncDecWidget = tg.RenderFunc(func(c tg.Context) tg.UI { var IncDecWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
var ( var (
kbd *tg.InlineCompo kbd *tg.InlineCompo

View file

@ -4,8 +4,8 @@ import (
"surdeus.su/core/tg" "surdeus.su/core/tg"
) )
var HomeButton = tg.Buttonf("Home").Go("/") var HomeButton = tg.Buttonf("Home").Go(StartWidget)
var BackButton = tg.Buttonf("Back").Go("-") var BackButton = tg.Buttonf("Back").Go(tg.Back)
var BackKeyboard = tg.NewKeyboard().Row( var BackKeyboard = tg.NewKeyboard().Row(
BackButton, BackButton,
) )

View file

@ -4,10 +4,6 @@ import (
"surdeus.su/core/tg" "surdeus.su/core/tg"
) )
const (
LocationPath tg.Path = "location"
)
var LocationWidget = tg.RenderFunc(func(c tg.Context) tg.UI { var LocationWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
return tg.UI{ return tg.UI{
tg.Messagef( tg.Messagef(

View file

@ -22,67 +22,36 @@ func ExtractSessionData(c tg.Context) *SessionData {
var beh = tg.NewBehaviour().SetInit(tg.Func(func(c tg.Context) { var beh = tg.NewBehaviour().SetInit(tg.Func(func(c tg.Context) {
// The session initialization. // The session initialization.
c.SetSessionData(&SessionData{}) c.SetSessionData(&SessionData{})
})).SetRootNode(tg.NewRootNode( })).SetRootWidget(
// The "/" widget. // Setting as the most top
StartWidget, // widget command handling
// so we can call them at any screen.
tg.NewNode( tg.NewCommandCompo().SetUsage(
PanelPath,
PanelWidget,
),
tg.NewNode(
MutateMessagesPath,
MutateMessagesWidget,
tg.NewNode(
UpperCasePath,
MutateMessagesToUpperCaseWidget,
),
tg.NewNode(
LowerCasePath,
MutateMessagesToLowerCaseWidget,
),
tg.NewNode(
EscapePath,
MutateMessagesEscapeWidget,
),
),
tg.NewNode(
IncDecPath,
IncDecWidget,
),
tg.NewNode(
LocationPath,
LocationWidget,
),
)).SetRootWidget(tg.NewCommandCompo().SetUsage(
UsageAction, UsageAction,
).SetPreStart( ).SetPreStart(
PreStartAction, PreStartAction,
).SetCommands( ).SetCommands(
BotCommands..., BotCommands...,
)) ),
)
func main() { func main() {
token := os.Getenv("BOT_TOKEN") token := os.Getenv("BOT_TOKEN")
bot, err := tg.NewBot(token) bot, err := tg.NewBot(token)
if err != nil { if err != nil {
log.Panic(err) log.Fatalf("tg.NewBot(...): %s", err)
} }
bot = bot.SetBehaviour(beh) bot = bot.SetBehaviour(beh)
//SetDebug(true) //bot.API().Debug = true
bot.SetData(&BotData{ bot.SetData(&BotData{
Name: "Jay", Name: "Jay",
}) })
log.Printf("Authorized on account %s", bot.Api().Self.UserName) log.Printf("Authorized on account %s", bot.API().Self.UserName)
err = bot.Run() err = bot.Run()
if err != nil { if err != nil {
panic(err) log.Fatalf("bot.Run(...): %s", err)
} }
} }

View file

@ -43,22 +43,15 @@ func (w *MutateMessageCompo) Filter(u tg.Update) bool {
return false return false
} }
const ( var MutateMessagesWidget= tg.RenderFunc(func(c tg.Context) tg.UI {
UpperCasePath tg.Path = "upper-case"
LowerCasePath = "lower-case"
EscapePath = "escape"
MutateMessagesPath = "mutate-messages"
)
var MutateMessagesWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
return tg.UI{ return tg.UI{
tg.Messagef( tg.Messagef(
"Choose widget to mutate strings", "Choose widget to mutate strings",
).Reply( ).Reply(
tg.NewKeyboard().Row( tg.NewKeyboard().Row(
tg.Buttonf("Upper case").Go(UpperCasePath), tg.Buttonf("Upper case").Go(UpperCaseWidget),
tg.Buttonf("Lower case").Go(LowerCasePath), tg.Buttonf("Lower case").Go(LowerCaseWidget),
tg.Buttonf("Escape chars").Go(EscapePath), tg.Buttonf("Escape chars").Go(EscapeWidget),
).Row( ).Row(
BackButton, BackButton,
).Reply(), ).Reply(),
@ -66,7 +59,7 @@ var MutateMessagesWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
} }
}) })
var MutateMessagesToLowerCaseWidget = tg.RenderFunc(func(c tg.Context) tg.UI { var LowerCaseWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
return tg.UI{ return tg.UI{
tg.Messagef( tg.Messagef(
"Type a string and the bot will convert it to lower case", "Type a string and the bot will convert it to lower case",
@ -77,7 +70,7 @@ var MutateMessagesToLowerCaseWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
} }
}) })
var MutateMessagesToUpperCaseWidget = tg.RenderFunc(func(c tg.Context) tg.UI { var UpperCaseWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
return tg.UI{ return tg.UI{
tg.Messagef( tg.Messagef(
"Type a string and the bot will convert it to upper case", "Type a string and the bot will convert it to upper case",
@ -87,7 +80,7 @@ var MutateMessagesToUpperCaseWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
NewMutateMessageCompo(strings.ToUpper), NewMutateMessageCompo(strings.ToUpper),
} }
}) })
var MutateMessagesEscapeWidget = tg.RenderFunc(func(c tg.Context) tg.UI { var EscapeWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
return tg.UI{ return tg.UI{
tg.Messagef( tg.Messagef(
"Type a string and the bot will escape characters in it", "Type a string and the bot will escape characters in it",

View file

@ -4,11 +4,7 @@ import (
"surdeus.su/core/tg" "surdeus.su/core/tg"
) )
const ( var DynamicPanelWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
PanelPath tg.Path = "panel"
)
var PanelWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
var ( var (
n = 0 n = 0
ln = 4 ln = 4

View file

@ -5,36 +5,30 @@ import (
"fmt" "fmt"
) )
const (
StartAbsPath tg.Path = "/"
)
var StartWidget = tg.RenderFunc(func(c tg.Context) tg.UI { var StartWidget = tg.RenderFunc(func(c tg.Context) tg.UI {
return tg.UI{ return tg.UI{
tg.Messagef( tg.Messagef(
fmt.Sprint( fmt.Sprint(
"Hello, %s!\n", "Hello, %s!",
"The testing bot started!\n", "The testing bot started!",
"You can see the basics of usage in the ", "You can see the basics of usage in the ",
"cmd/test/main.go file!", "cmd/test/main.go file and other files in the cmd/test!",
), ),
c.CallbackUpdate().SentFrom().UserName, c.CallbackUpdate().SentFrom().UserName,
).Inline( ).Inline(
tg.NewKeyboard().Row( tg.NewKeyboard().Row(
tg.Buttonf("TeleGopher Vultras page"). tg.Buttonf("TeleGopher surdeus.su page").
WithUrl("https://vultras.su/core/tg"), WithUrl("https://surdeus.su/core/tg"),
).Inline(), ).Inline(),
), ),
tg.Messagef("Choose your interest").Reply( tg.Messagef("Choose your interest").Reply(
tg.NewKeyboard().Row( tg.NewKeyboard().List(
tg.Buttonf("Inc/Dec").Go(IncDecPath), tg.Buttonf("Inc/Dec").Go(IncDecWidget),
).Row( tg.Buttonf("Mutate messages").Go(MutateMessagesWidget),
tg.Buttonf("Mutate messages").Go(MutateMessagesPath), tg.Buttonf("Send location").Go(LocationWidget),
).Row( tg.Buttonf("Dynamic panel").Go(DynamicPanelWidget),
tg.Buttonf("Send location").Go(LocationPath), tg.Buttonf("Check panic").Go(nil),
).Row(
tg.Buttonf("Dynamic panel").Go(PanelPath),
).Reply(), ).Reply(),
), ),

View file

@ -4,6 +4,7 @@ import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
type CommandType uint8 type CommandType uint8
const ( const (
PrivateCommandType CommandType = iota PrivateCommandType CommandType = iota
@ -44,7 +45,7 @@ func (c Command) WithWidget(w Widget) Command {
} }
// Convert command into the tgbotapi.BotCommand // Convert command into the tgbotapi.BotCommand
func (c Command) ToApi() tgbotapi.BotCommand { func (c Command) ToAPI() tgbotapi.BotCommand {
ret := tgbotapi.BotCommand{} ret := tgbotapi.BotCommand{}
ret.Command = string(c.Name) ret.Command = string(c.Name)
ret.Description = c.Description ret.Description = c.Description
@ -52,14 +53,14 @@ func (c Command) ToApi() tgbotapi.BotCommand {
} }
// Simple command to go to another screen. // Simple command to go to another screen.
func (c Command) Go(pth Path) Command { func (c Command) Go(pth Widget) Command {
return c.WithAction(ScreenGo{ return c.WithAction(WidgetGo{
Path: pth, Path: pth,
}) })
} }
func (c Command) GoWithArg(pth Path, arg any) Command { func (c Command) GoWithArg(pth Widget, arg any) Command {
return c.WithAction(ScreenGo{ return c.WithAction(WidgetGo{
Path: pth, Path: pth,
Arg: arg, Arg: arg,
}) })
@ -125,7 +126,7 @@ func (compo *CommandCompo) Serve(c Context) {
// First should bring the new command into the action. // First should bring the new command into the action.
c.Bot().DeleteCommands() c.Bot().DeleteCommands()
err := c.Bot().SetCommands( err := c.Bot().SetCommands(
tgbotapi.NewBotCommandScopeChat(c.SessionId().ToApi()), tgbotapi.NewBotCommandScopeChat(c.SessionID().ToAPI()),
compo.Commands, compo.Commands,
) )
@ -135,11 +136,12 @@ func (compo *CommandCompo) Serve(c Context) {
var cmdUpdates *UpdateChan var cmdUpdates *UpdateChan
for u := range c.Input() { for u := range c.Input() {
if c.Path() == "" && u.Message != nil { if c.Path() == nil && u.Message != nil {
// Skipping and executing the preinit action // Skipping and executing the preinit action
// while we have the empty screen. // while we have the empty screen.
// E. g. the session did not start. // E. g. the session did not start.
if !(u.Message.IsCommand() && u.Message.Command() == "start") { if !u.Message.IsCommand() ||
u.Message.Command() != "start" {
c.WithUpdate(u).Run(compo.PreStart) c.WithUpdate(u).Run(compo.PreStart)
continue continue
} }

View file

@ -30,7 +30,7 @@ type Context struct {
// maybe you will find another usage for this. // maybe you will find another usage for this.
// Returns users context by specified session ID // Returns users context by specified session ID
// or false if the user is not logged in. // or false if the user is not logged in.
func (c Context) As(sid SessionId) (Context, bool) { func (c Context) As(sid SessionID) (Context, bool) {
s, ok := c.Bot().GotSession(sid) s, ok := c.Bot().GotSession(sid)
if !ok { if !ok {
return Context{}, false return Context{}, false
@ -58,6 +58,9 @@ func (f Func) Render(_ Context) UI {
} }
} }
// The type represents type
// of current context the processing is happening
// in.
type ContextType uint8 type ContextType uint8
const ( const (
NoContextType ContextType = iota NoContextType ContextType = iota
@ -69,7 +72,14 @@ const (
func (c Context) serve() { func (c Context) serve() {
beh := c.Bot().behaviour beh := c.Bot().behaviour
c.Run(beh.Init) c.Run(beh.Init)
for {
defer func(){
if err := recover() ; err != nil {
// Need to add some handling later.
}
}()
beh.Root.Serve(c) beh.Root.Serve(c)
}
} }
func (c Context) Arg() any { func (c Context) Arg() any {
@ -85,12 +95,12 @@ func (c Context) Run(a Action) {
// Sends to the Sendable object to the session user. // Sends to the Sendable object to the session user.
func (c Context) Send(v Sendable) (*Message, error) { func (c Context) Send(v Sendable) (*Message, error) {
config := v.SendConfig(c.SessionId(), c.Bot()) config := v.SendConfig(c.SessionID(), c.Bot())
if config.Error != nil { if config.Error != nil {
return nil, config.Error return nil, config.Error
} }
msg, err := c.Bot().Api().Send(config.ToApi()) msg, err := c.Bot().API().Send(config.ToAPI())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -172,14 +182,16 @@ func (c Context) CallbackUpdate() *Update {
} }
// Returns the reader for specified file ID and path. // Returns the reader for specified file ID and path.
func (c Context) GetFile(fileId FileId) (io.ReadCloser, string, error) { func (c Context) GetFile(fileID FileID) (io.ReadCloser, string, error) {
file, err := c.Bot().Api().GetFile(tgbotapi.FileConfig{FileID:string(fileId)}) file, err := c.Bot().API().GetFile(tgbotapi.FileConfig{
FileID: string(fileID),
})
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
r, err := http.Get(fmt.Sprintf( r, err := http.Get(fmt.Sprintf(
"https://api.telegram.org/file/bot%s/%s", "https://api.telegram.org/file/bot%s/%s",
c.Bot().Api().Token, c.Bot().API().Token,
file.FilePath, file.FilePath,
)) ))
if err != nil { if err != nil {
@ -193,8 +205,8 @@ func (c Context) GetFile(fileId FileId) (io.ReadCloser, string, error) {
} }
// Reads all the content from the specified file. // Reads all the content from the specified file.
func (c Context) ReadFile(fileId FileId) ([]byte, string, error) { func (c Context) ReadFile(fileID FileID) ([]byte, string, error) {
file, pth, err := c.GetFile(fileId) file, pth, err := c.GetFile(fileID)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@ -240,10 +252,9 @@ func (c Context) runWidget(widget Widget, arg any) (*UpdateChan, error) {
return nil, EmptyWidgetErr return nil, EmptyWidgetErr
} }
pth := c.Path()
compos := widget.Render(c.WithArg(arg)) compos := widget.Render(c.WithArg(arg))
// Leave if changed path or components are empty. // Leave if changed path or components are empty.
if compos == nil || pth != c.Path() { if compos == nil {
return nil, EmptyCompoErr return nil, EmptyCompoErr
} }
chns := make([]*UpdateChan, len(compos)) chns := make([]*UpdateChan, len(compos))
@ -289,53 +300,42 @@ func (c Context) runWidget(widget Widget, arg any) (*UpdateChan, error) {
} }
// Simple go without an argument for context. // Simple go without an argument for context.
func (c Context) Go(pth Path) error { func (c Context) Go(pth Widget) error {
return c.GoWithArg(pth, nil) return c.GoWithArg(pth, nil)
} }
// Changes screen of user to the Id one. // Go to the specified widget with the
// Also gives the arg to the widget calling // specificed argument.
// contexts. func (c Context) GoWithArg(pth Widget, arg any) error {
func (c Context) GoWithArg(pth Path, arg any) error {
var err error var err error
if pth == "" { if pth == nil {
c.session.pathHistory = []Path{} c.session.pathHistory = []Widget{}
return nil return nil
} }
var back bool var back bool
if pth == "-" { if pth == Back {
if len(c.session.pathHistory) < 2 { if len(c.session.pathHistory) < 2 {
return c.GoWithArg("", arg) return c.GoWithArg(nil, arg)
} }
pth = c.session.pathHistory[len(c.session.pathHistory)-2] pth = c.session.pathHistory[len(c.session.pathHistory)-2]
c.session.pathHistory = c.session.pathHistory[:len(c.session.pathHistory)-1] c.session.pathHistory =
} c.session.pathHistory[:len(c.session.pathHistory)-1]
// Getting the screen and changing to back = true
// then executing its widget.
if !pth.IsAbs() {
pth = (c.Path() + "/" + pth).Clean()
} }
if !c.PathExist(pth) { if !back {
return ScreenNotExistErr
}
if !back && c.Path() != pth {
c.session.pathHistory = append(c.session.pathHistory, pth) c.session.pathHistory = append(c.session.pathHistory, pth)
} }
// Stopping the current widget. // Stopping the current widget.
screen := c.Bot().behaviour.Screens[pth]
c.session.skippedUpdates.Close() c.session.skippedUpdates.Close()
if screen.Widget != nil { // Running the new one.
c.session.skippedUpdates, err = c.runWidget(screen.Widget, arg) c.session.skippedUpdates, err = c.runWidget(pth, arg)
if err != nil { if err != nil {
return err return err
} }
} else {
return NoWidgetForScreenErr
}
return nil return nil
} }
@ -352,8 +352,8 @@ func (c Context) SessionData() any {
return c.session.Data return c.session.Data
} }
func (c Context) SessionId() SessionId { func (c Context) SessionID() SessionID {
return c.session.Id return c.session.ID
} }
func (c Context) SessionScope() SessionScope { func (c Context) SessionScope() SessionScope {
@ -372,20 +372,15 @@ func (c Context) Bot() *Bot {
return c.session.bot return c.session.bot
} }
// Returns true if the path exists and false otherwise.
func (c Context) PathExist(pth Path) bool {
return c.session.bot.behaviour.PathExist(pth)
}
// Return context's session's path history. // Return context's session's path history.
func (c Context) PathHistory() []Path { func (c Context) PathHistory() []Widget {
return c.session.pathHistory return c.session.pathHistory
} }
func (c Context) Path() Path { func (c Context) Path() Widget {
ln := len(c.session.pathHistory) ln := len(c.session.pathHistory)
if ln == 0 { if ln == 0 {
return "" return nil
} }
return c.session.pathHistory[ln-1] return c.session.pathHistory[ln-1]
} }

4
devel-loop Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
wgo sh -c './btest && ./exe/test'

View file

@ -10,6 +10,7 @@ import (
"github.com/go-telegram-bot-api/telegram-bot-api/v5" "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
type FileID string
type FileConfig = tgbotapi.FileConfig type FileConfig = tgbotapi.FileConfig
type PhotoConfig = tgbotapi.PhotoConfig type PhotoConfig = tgbotapi.PhotoConfig
type FileType int type FileType int
@ -107,10 +108,10 @@ func (f *File) SendData() string {
} }
func (f *File) SendConfig( func (f *File) SendConfig(
sid SessionId, bot *Bot, sid SessionID, bot *Bot,
) (SendConfig) { ) (SendConfig) {
var config SendConfig var config SendConfig
cid := sid.ToApi() cid := sid.ToAPI()
switch f.Type() { switch f.Type() {
case PhotoFileType: case PhotoFileType:
@ -119,7 +120,7 @@ func (f *File) SendConfig(
config.Chattable = photo config.Chattable = photo
case DocumentFileType: case DocumentFileType:
doc := tgbotapi.NewDocument(sid.ToApi(), f) doc := tgbotapi.NewDocument(sid.ToAPI(), f)
doc.Caption = f.caption doc.Caption = f.caption
config.Chattable = doc config.Chattable = doc
default: default:

29
go.go
View file

@ -1,22 +1,33 @@
package tg package tg
func Go(pth Path) UI { func Go(pth Widget) UI {
return UI{ return UI{
GoWidget(pth), WidgetGo{
Path: pth,
},
} }
} }
type GoWidget string // The type implements changing current path to the widget.
// Implementing the Server interface. type WidgetGo struct {
func (widget GoWidget) Serve(c Context) { Path Widget
c.input.Close() Arg any
c.Go(Path(widget))
} }
func (widget GoWidget) Render(c Context) UI { func (sc WidgetGo) Act(c Context) {
c.GoWithArg(sc.Path, sc.Arg)
}
// Implementing the Server interface.
func (widget WidgetGo) Serve(c Context) {
c.input.Close()
c.Go(widget)
}
func (widget WidgetGo) Render(c Context) UI {
return UI{widget} return UI{widget}
} }
func (widget GoWidget) Filter(u Update) bool { func (widget WidgetGo) Filter(u Update) bool {
return true return true
} }

View file

@ -11,7 +11,7 @@ type Inline struct {
} }
// Convert the inline keyboard to markup for the tgbotapi. // Convert the inline keyboard to markup for the tgbotapi.
func (kbd Inline) ToApi() tgbotapi.InlineKeyboardMarkup { func (kbd Inline) ToAPI() tgbotapi.InlineKeyboardMarkup {
rows := [][]tgbotapi.InlineKeyboardButton{} rows := [][]tgbotapi.InlineKeyboardButton{}
for _, row := range kbd.Rows { for _, row := range kbd.Rows {
if row == nil { if row == nil {
@ -38,12 +38,12 @@ type InlineCompo struct {
// Implementing the Sendable interface. // Implementing the Sendable interface.
func (compo *InlineCompo) SendConfig( func (compo *InlineCompo) SendConfig(
sid SessionId, bot *Bot, sid SessionID, bot *Bot,
) (SendConfig) { ) (SendConfig) {
sendConfig := compo.MessageCompo.SendConfig(sid, bot) sendConfig := compo.MessageCompo.SendConfig(sid, bot)
msg := sendConfig.Chattable.(tgbotapi.MessageConfig) msg := sendConfig.Chattable.(tgbotapi.MessageConfig)
if len(compo.Inline.Rows) > 0 { if len(compo.Inline.Rows) > 0 {
msg.ReplyMarkup = compo.Inline.ToApi() msg.ReplyMarkup = compo.Inline.ToAPI()
} }
sendConfig.Chattable = msg sendConfig.Chattable = msg
@ -56,23 +56,23 @@ func (compo *InlineCompo) SendConfig(
func (compo *InlineCompo) Update(c Context) error { func (compo *InlineCompo) Update(c Context) error {
if compo.Message != nil { if compo.Message != nil {
var edit tgbotapi.Chattable var edit tgbotapi.Chattable
markup := compo.Inline.ToApi() markup := compo.Inline.ToAPI()
ln := len(markup.InlineKeyboard) ln := len(markup.InlineKeyboard)
if ln == 0 || compo.Inline.Rows == nil { if ln == 0 || compo.Inline.Rows == nil {
edit = tgbotapi.NewEditMessageText( edit = tgbotapi.NewEditMessageText(
c.SessionId().ToApi(), c.SessionID().ToAPI(),
compo.Message.MessageID, compo.Message.MessageID,
compo.Text, compo.Text,
) )
} else { } else {
edit = tgbotapi.NewEditMessageTextAndMarkup( edit = tgbotapi.NewEditMessageTextAndMarkup(
c.SessionId().ToApi(), c.SessionID().ToAPI(),
compo.Message.MessageID, compo.Message.MessageID,
compo.Text, compo.Text,
markup, markup,
) )
} }
msg, err := c.Bot().Api().Send(edit) msg, err := c.Bot().API().Send(edit)
if err != nil { if err != nil {
return err return err
} }
@ -113,7 +113,7 @@ func (compo *InlineCompo) OnOneUpdate(c Context, u Update) error {
) )
data := u.CallbackQuery.Data data := u.CallbackQuery.Data
_, err := c.Bot().Api().Request(cb) _, err := c.Bot().API().Request(cb)
if err != nil { if err != nil {
return err return err
} }

View file

@ -10,7 +10,7 @@ type InvoiceCompo struct {
} }
func (compo *InvoiceCompo) SendConfig( func (compo *InvoiceCompo) SendConfig(
sid SessionId, bot *Bot, sid SessionID, bot *Bot,
) (*SendConfig) { ) (*SendConfig) {
return nil return nil
} }

View file

@ -12,9 +12,9 @@ type LocationCompo struct {
} }
func (compo *LocationCompo) SendConfig( func (compo *LocationCompo) SendConfig(
sid SessionId, bot *Bot, sid SessionID, bot *Bot,
) (SendConfig) { ) (SendConfig) {
cid := sid.ToApi() cid := sid.ToAPI()
location := tgbotapi.NewLocation( location := tgbotapi.NewLocation(
cid, cid,
compo.Latitude, compo.Latitude,

View file

@ -34,11 +34,11 @@ func Escape2(str string) string {
// Call the function after the message was sent. // Call the function after the message was sent.
func (compo *MessageCompo) Update(c Context) error { func (compo *MessageCompo) Update(c Context) error {
edit := tgbotapi.NewEditMessageText( edit := tgbotapi.NewEditMessageText(
c.Session().Id.ToApi(), c.Session().ID.ToAPI(),
compo.Message.MessageID, compo.Message.MessageID,
compo.Text, compo.Text,
) )
msg, err := c.Bot().Api().Send(edit) msg, err := c.Bot().API().Send(edit)
if err != nil { if err != nil {
return err return err
} }
@ -49,8 +49,8 @@ func (compo *MessageCompo) Update(c Context) error {
// Calling the method removes the message on the client side // Calling the method removes the message on the client side
// and sets the Message in the component to nil. // and sets the Message in the component to nil.
func (compo *MessageCompo) Delete(c Context) error { func (compo *MessageCompo) Delete(c Context) error {
cfg := tgbotapi.NewDeleteMessage(c.session.Id.ToApi(), compo.Message.MessageID) cfg := tgbotapi.NewDeleteMessage(c.session.ID.ToAPI(), compo.Message.MessageID)
_, err := c.Bot().Api().Send(cfg) _, err := c.Bot().API().Send(cfg)
if err != nil { if err != nil {
return err return err
} }
@ -121,7 +121,7 @@ func (msg *MessageCompo) Location(
// Implementing the Sendable interface. // Implementing the Sendable interface.
func (compo *MessageCompo) SendConfig( func (compo *MessageCompo) SendConfig(
sid SessionId, bot *Bot, sid SessionID, bot *Bot,
) (SendConfig) { ) (SendConfig) {
var ( var (
ret SendConfig ret SendConfig
@ -136,7 +136,7 @@ func (compo *MessageCompo) SendConfig(
text = compo.Text text = compo.Text
} }
msg := tgbotapi.NewMessage(sid.ToApi(), text) msg := tgbotapi.NewMessage(sid.ToAPI(), text)
msg.ParseMode = compo.ParseMode msg.ParseMode = compo.ParseMode
ret.Chattable = msg ret.Chattable = msg

View file

@ -64,7 +64,7 @@ type ReplyCompo struct {
// Implementing the sendable interface. // Implementing the sendable interface.
func (compo *ReplyCompo) SendConfig( func (compo *ReplyCompo) SendConfig(
sid SessionId, bot *Bot, sid SessionID, bot *Bot,
) (SendConfig) { ) (SendConfig) {
sendConfig := compo.MessageCompo.SendConfig(sid, bot) sendConfig := compo.MessageCompo.SendConfig(sid, bot)

View file

@ -1,48 +1,32 @@
package tg package tg
import ( type WidgetSpecial int
"path" const (
widgetEmpty WidgetSpecial = iota
widgetBack
) )
// The type implements changing screen to the underlying ScreenId
type ScreenGo struct { func (w WidgetSpecial) Render(_ Context) UI {
Path Path return nil
Arg any
} }
func (sc ScreenGo) Act(c Context) { var (
c.GoWithArg(sc.Path, sc.Arg) Back = Widget(widgetBack)
} )
// The same as Act. /*// Unique identifier for the screen.
func (sc ScreenGo) Serve(c Context) { type Path int
sc.Act(c) const (
} PathEmpty Path = 0
// Going to the path returns
// Unique identifier for the screen // a context to the previous screen.
// and relative paths to the screen. PathBack Path = -1
type Path string )
// Returns true if the path is empty. // Returns true if the path is empty.
func (p Path) IsEmpty() bool { func (p Path) IsEmpty() bool {
return p == "" return p == 0
}
// Returns true if the path is absolute.
func (p Path) IsAbs() bool {
if len(p) == 0 {
return false
}
return p[0] == '/'
}
func (p Path) Dir() Path {
return Path(path.Dir(string(p)))
}
// Clean the path deleting exceed ., .. and / .
func (p Path) Clean() Path {
return Path(path.Clean(string(p)))
} }
// Screen statement of the bot. // Screen statement of the bot.
@ -64,10 +48,10 @@ type Node struct {
Path Path Path Path
Screen *Screen Screen *Screen
Subs []*Node Subs []*Node
} }*/
// Return new root node with the specified widget in the screen. // Return new root node with the specified widget in the screen.
func NewRootNode(widget Widget, subs ...*Node) *RootNode { /*func NewRootNode(widget Widget, subs ...*Node) *RootNode {
ret := &RootNode{} ret := &RootNode{}
ret.Screen = NewScreen(widget) ret.Screen = NewScreen(widget)
ret.Subs = subs ret.Subs = subs
@ -114,15 +98,13 @@ func (n *Node) ScreenMap(root Path) ScreenMap {
} }
} }
return m return m
} }*/
// Map structure for the screens.
type ScreenMap map[Path] *Screen
// Returns the new screen with specified name and widget. // Returns the new screen with specified name and widget.
func NewScreen(widget Widget) *Screen { /*func NewScreen(widget Widget) *Screen {
return &Screen{ return &Screen{
Widget: widget, Widget: widget,
} }
} }*/

View file

@ -10,7 +10,7 @@ type MessageId int64
// way to define what message will be // way to define what message will be
// sent to the side of a user. // sent to the side of a user.
type Sendable interface { type Sendable interface {
SendConfig(SessionId, *Bot) (SendConfig) SendConfig(SessionID, *Bot) (SendConfig)
SetMessage(*Message) SetMessage(*Message)
} }
@ -25,7 +25,7 @@ type SendConfig struct {
type MessageMap map[string] *Message type MessageMap map[string] *Message
// Convert to the bot.Api.Send format. // Convert to the bot.Api.Send format.
func (config SendConfig) ToApi() tgbotapi.Chattable { func (config SendConfig) ToAPI() tgbotapi.Chattable {
return config.Chattable return config.Chattable
} }

View file

@ -2,12 +2,12 @@ package tg
// The type represents map of sessions using // The type represents map of sessions using
// as key. // as key.
type SessionMap map[SessionId]*Session type SessionMap map[SessionID]*Session
// Add new empty session by it's ID. // Add new empty session by it's ID.
func (sm SessionMap) Add( func (sm SessionMap) Add(
bot *Bot, bot *Bot,
sid SessionId, sid SessionID,
scope SessionScope, scope SessionScope,
) *Session { ) *Session {
ret := NewSession(bot, sid, scope) ret := NewSession(bot, sid, scope)
@ -27,32 +27,32 @@ const (
// Represents unique value to identify chats. // Represents unique value to identify chats.
// In fact is simply ID of the chat. // In fact is simply ID of the chat.
type SessionId int64 type SessionID int64
// Convert the SessionId to Telegram API's type. // Convert the SessionID to Telegram API's type.
func (si SessionId) ToApi() int64 { func (si SessionID) ToAPI() int64 {
return int64(si) return int64(si)
} }
// The type represents current state of // The type represents current state of
// user interaction per each of them. // user interaction per each of them.
type Session struct { type Session struct {
// Id of the chat of the user. // ID of the chat of the user.
Id SessionId ID SessionID
Scope SessionScope Scope SessionScope
// Custom value for each user. // Custom value for each user.
Data any Data any
bot *Bot bot *Bot
pathHistory []Path pathHistory []Widget
skippedUpdates *UpdateChan skippedUpdates *UpdateChan
updates *UpdateChan updates *UpdateChan
} }
// Return new empty session. // Return new empty session.
func NewSession(bot *Bot, id SessionId, scope SessionScope) *Session { func NewSession(bot *Bot, id SessionID, scope SessionScope) *Session {
ret := &Session{} ret := &Session{}
ret.Id = id ret.ID = id
ret.Scope = scope ret.Scope = scope
ret.bot = bot ret.bot = bot
ret.updates = NewUpdateChan() ret.updates = NewUpdateChan()

View file

@ -2,7 +2,6 @@ package tg
import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" import tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
type FileId string
type Update struct { type Update struct {
tgbotapi.Update tgbotapi.Update
@ -64,8 +63,8 @@ func (u Update) HasDocument() bool {
u.Message.Document != nil u.Message.Document != nil
} }
func (u Update) DocumentId() FileId { func (u Update) DocumentID() FileID {
return FileId(u.Update.Message.Document.FileID) return FileID(u.Update.Message.Document.FileID)
} }
func (u *Update) DocumentName() string { func (u *Update) DocumentName() string {
@ -85,10 +84,10 @@ func (u Update) HasPhotos() bool {
len(u.Message.Photo) != 0 len(u.Message.Photo) != 0
} }
func (u Update) PhotoIds() []FileId { func (u Update) PhotoIDs() []FileID {
ret := make([]FileId, len(u.Message.Photo)) ret := make([]FileID, len(u.Message.Photo))
for i, photo := range u.Message.Photo { for i, photo := range u.Message.Photo {
ret[i] = FileId(photo.FileID) ret[i] = FileID(photo.FileID)
} }
return ret return ret
} }