diff --git a/cmd/test/main.go b/cmd/test/main.go index 95b3931..2578832 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -103,34 +103,32 @@ WithInitFunc(func(c *tg.Context) { c.Session.Data = &SessionData{} }).WithRootNode(tg.NewRootNode( // The "/" widget. - tg.WidgetFunc(func(c *tg.Context) tg.UIs { - return tg.UIs{ + tg.WidgetFunc(func(c *tg.Context) tg.UIs {return tg.UIs{ - tg.NewKeyboard().Row( - tg.NewButton("GoT Github page"). - WithUrl("https://github.com/mojosa-software/got"), - ).Inline().Widget( - fmt.Sprintf( - "Hello, %s" - "The testing bot started!\n", - "You can see the basics of usage in the ", - "cmd/test/main.go file!", - c.SentFrom().UserName, - ), + tg.NewKeyboard().Row( + tg.NewButton("GoT Github page"). + WithUrl("https://github.com/mojosa-software/got"), + ).Inline().Widget( + fmt.Sprintf( + "Hello, %s!\n" + "The testing bot started!\n", + "You can see the basics of usage in the ", + "cmd/test/main.go file!", + c.SentFrom().UserName, ), + ), - tg.NewKeyboard().Row( - tg.NewButton("Inc/Dec").Go("/inc-dec"), - ).Row( - tg.NewButton("Mutate messages").Go("/mutate-messages"), - ).Row( - tg.NewButton("Send location").Go("/send-location"), - ).Reply().Widget( - "Choose the point of your interest", - ), + tg.NewKeyboard().Row( + tg.NewButton("Inc/Dec").Go("/inc-dec"), + ).Row( + tg.NewButton("Mutate messages").Go("/mutate-messages"), + ).Row( + tg.NewButton("Send location").Go("/send-location"), + ).Reply().Widget( + "Choose the point of your interest", + ), - } - ) + }}), tg.NewNode( "mutate-messages", tg.NewPage().WithReply( diff --git a/tg/bot.go b/tg/bot.go index 9d0c5ed..f7c20c7 100644 --- a/tg/bot.go +++ b/tg/bot.go @@ -25,9 +25,9 @@ type Bot struct { groupBehaviour *GroupBehaviour // Bot behaviour in channels. channelBehaviour *ChannelBehaviour + contexts map[SessionId] *context sessions SessionMap groupSessions GroupSessionMap - } // Return the new bot with empty sessions and behaviour. @@ -50,8 +50,13 @@ func (bot *Bot) Debug(debug bool) *Bot { // Send the Renderable to the specified session client side. // Can be used for both group and private sessions. func (bot *Bot) Send( - sid SessionId, v Renderable, + sid SessionId, v Sendable, ) (*Message, error) { + c, ok := bot.contexts[sid] + if !ok { + return nil, ContextNotExistErr + } + config := v.Render(sid, bot) if config.Error != nil { return nil, config.Error @@ -64,7 +69,7 @@ func (bot *Bot) Send( return &msg, nil } -func (bot *Bot) Render( +/*func (bot *Bot) Render( sid SessionId, r Renderable, ) (MessageMap, error) { configs := r.Render(sid, bot) @@ -84,7 +89,7 @@ func (bot *Bot) Render( messages[config.Name] = &msg } return messages, nil -} +}*/ func (bot *Bot) GetSession( sid SessionId, @@ -210,57 +215,40 @@ func (bot *Bot) Run() error { // The function handles updates supposed for the private // chat with the bot. func (bot *Bot) handlePrivate(updates chan *Update) { - chans := make(map[SessionId] *UpdateChan ) var sid SessionId for u := range updates { sid = SessionId(u.FromChat().ID) - // Create new session if the one does not exist - // for this user. - - // Making the bot ignore anything except "start" - // before the session started - session, sessionOk := bot.sessions[sid] - chn, chnOk := chans[sid] - if sessionOk { - // Creating new goroutine for - // the session that exists - // but has none. - if !chnOk { - ctx := &context{ - Bot: bot, - Session: session, - } - chn := NewUpdateChan() - chans[sid] = chn - go (&Context{ - context: ctx, - Update: u, - input: chn, - }).serve() - } - } else if u.Message != nil { - // Create session on any message + ctx, ctxOk := bot.contexts[sid] + if u.Message != nil && !ctxOk { + // Create context on any message // if we have no one. - bot.sessions.Add(sid) - lsession := bot.sessions[sid] - ctx := &context{ - Bot: bot, - Session: lsession, + session, sessionOk := bot.sessions[sid] + if !sessionOk { + // Creating session if we have none. + session = bot.sessions.Add(sid) } - chn := NewUpdateChan() - chans[sid] = chn + session = bot.sessions[sid] + ctx = &context{ + Bot: bot, + Session: session, + scope: PrivateContextScope, + updates: NewUpdateChan(), + } + if !ctxOk { + bot.contexts[sid] = ctx + } + go (&Context{ context: ctx, Update: u, - input: chn, + input: ctx.updates, }).serve() + ctx.updates.Send(u) + continue } - chn, ok := chans[sid] - // The bot MUST get the "start" command. - // It will do nothing otherwise. - if ok { - chn.Send(u) + if ctxOk { + ctx.updates.Send(u) } } } diff --git a/tg/command.go b/tg/command.go index 048e6b2..9730798 100644 --- a/tg/command.go +++ b/tg/command.go @@ -40,7 +40,7 @@ func (c *Command) WithWidget(w Widget) *Command { return c } -func (c *Command) WidgetFunc(fn WidgetFunc) *Command { +func (c *Command) WidgetFunc(fn Func) *Command { return c.WithWidget(fn) } @@ -195,7 +195,7 @@ func (widget *CommandWidget) Serve(c *Context) { c.Run(cmd.Action, u) if cmd.Widget != nil { cmdUpdates.Close() - cmdUpdates = c.RunWidget(cmd.Widget) + cmdUpdates = c.runWidget(cmd.Widget) } continue } diff --git a/tg/compo.go b/tg/compo.go index 918b35c..4820b5d 100644 --- a/tg/compo.go +++ b/tg/compo.go @@ -1,32 +1,34 @@ package tg -type UIs []UI -// The type describes dynamic screen widget. +// The type describes dynamic screen widget +// That can have multiple UI components. type Widget interface { - UIs(*Context) UIs + Render(*Context) UI } // The way to describe custom function based Widgets. -type WidgetFunc func(c *Context) UIs -func (fn WidgetFunc) UIs(c *Context) UIs { +type RenderFunc func(c *Context) UI +func (fn RenderFunc) Uis(c *Context) UI { return fn(c) } +// The type that represents endpoint user interface +// via set of components that will work on the same screen +// in the same time. +type UI []Component + // The type describes interfaces // needed to be implemented to be endpoint handlers. -type UI interface { - Renderable +type Component interface { + // Optionaly component can implement the + // Renderable interface to automaticaly be sent to the + // user side. - SetMessage(*Message) - GetMessage() *Message Filterer - Server } -type UiFunc func() - // The type to embed into potential components. // Implements empty versions of interfaces // and contains diff --git a/tg/context.go b/tg/context.go index 84bfcc1..f8d1078 100644 --- a/tg/context.go +++ b/tg/context.go @@ -7,7 +7,8 @@ import ( //"path" ) -// General type function for faster typing. +// General type function to define actions, single component widgets +// and components themselves. type Func func(*Context) func (f Func) Act(c *Context) { f(c) @@ -15,16 +16,14 @@ func (f Func) Act(c *Context) { func (f Func) Serve(c *Context) { f(c) } - -// The way to determine where the context is -// related to. -type ContextScope uint8 -const ( - NoContextScope ContextScope = iota - PrivateContextScope - GroupContextScope - ChannelContextScope -) +func(f Func) Filter(_ *Context) bool { + return false +} +func (f Func) Render(_ *Context) UI { + return UI{ + f, + } +} type ContextType uint8 const ( @@ -40,6 +39,8 @@ type context struct { Session *Session // To reach the bot abilities inside callbacks. Bot *Bot + Type ContextType + updates *UpdateChan skippedUpdates *UpdateChan // Current screen ID. path, prevPath Path @@ -48,9 +49,7 @@ type context struct { // Goroutie function to handle each user. func (c *Context) serve() { beh := c.Bot.behaviour - if beh.Init != nil { - c.Run(beh.Init, c.Update) - } + c.Run(beh.Init) beh.Root.Serve(c) } @@ -67,7 +66,7 @@ func (c *Context) PrevPath() Path { return c.prevPath } -func (c *Context) Run(a Action, u *Update) { +func (c *Context) Run(a Action) { if a != nil { a.Act(c.Copy().WithUpdate(u)) } @@ -80,12 +79,6 @@ func (c *Context) Skip(u *Update) { c.skippedUpdates.Send(u) } -// Renders the Renedrable object to the side of client -// and returns the messages it sent. -func (c *Context) Render(v Renderable) (MessageMap, error) { - return c.Bot.Render(c.Session.Id, v) -} - // Sends to the Sendable object. func (c *Context) Send(v Sendable) (*Message, error) { return c.Bot.Send(c.Session.Id, v) @@ -134,16 +127,19 @@ func (c *Context) Copy() *Context { } 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 } @@ -160,9 +156,6 @@ func (af ActionFunc) Act(c *Context) { af(c) } - -type C = Context - // Changes screen of user to the Id one. func (c *Context) Go(pth Path, args ...any) error { // Getting the screen and changing to @@ -194,7 +187,7 @@ func (c *Context) PathExist(pth Path) bool { } // Run widget in background returning the new input channel for it. -func (c *Context) runWidget(widget Widget, args ...any) { +func (c *Context) runWidget(widget Widget, args ...any) *UpdateChan { if widget == nil { return nil } @@ -206,11 +199,19 @@ func (c *Context) runWidget(widget Widget, args ...any) { arg = args } + pth := c.Path() uis := widget.UI() + // Leave if changed path. + if pth != c.Path() { + return nil + } chns := make(map[UI] *UpdateChan) for _, ui := range uis { - msg := c.Send(ui.Render(c)) - ui.SetMessage(msg) + s, ok := ui.(Sendable) + if ok { + msg := c.Send(s.SendConfig(c)) + ui.SetMessage(msg) + } updates := NewUpdateChan() go func() { ui.Serve( @@ -219,19 +220,28 @@ func (c *Context) runWidget(widget Widget, args ...any) { WithArg(arg), ) // To let widgets finish themselves before - // the channel is closed. + // the channel is closed and close it by themselves. updates.Close() }() chns[ui] = updates } - for u := range c.skippedUpdates.Chan() { - for ui := range uis { - if !ui.Filter() { - chns[ui] <- u + ret := NewUpdateChan() + go func() { + for u := range ret { + for ui := range uis { + if !ui.Filter() { + chns[ui] <- u + } } } + ret.Close() + for _, chn := range chns { + chn.Close() + } } + + return ret } // Simple way to read strings for widgets. diff --git a/tg/errors.go b/tg/errors.go index f3caf0e..621d9fa 100644 --- a/tg/errors.go +++ b/tg/errors.go @@ -17,6 +17,7 @@ var ( EmptyKeyboardTextErr = errors.New("got empty text for a keyboard") ActionNotDefinedErr = errors.New("action was not defined") MapCollisionErr = errors.New("map collision occured") + ContextNotExistErr = errors.New("the context does not exist") ) func (wut WrongUpdateType) Error() string { diff --git a/tg/inline.go b/tg/inline.go index fca527e..3759a2f 100644 --- a/tg/inline.go +++ b/tg/inline.go @@ -9,14 +9,6 @@ type Inline struct { *Keyboard } -// Transform the keyboard to widget with the specified text. -func (kbd *Inline) Widget(text string) *InlineWidget { - ret := &InlineWidget{} - ret.Inline = kbd - ret.Text = text - return ret -} - // Convert the inline keyboard to markup for the tgbotapi. func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup { rows := [][]tgbotapi.InlineKeyboardButton{} @@ -31,6 +23,14 @@ func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup { return tgbotapi.NewInlineKeyboardMarkup(rows...) } +// Transform the keyboard to widget with the specified text. +func (kbd *Inline) Compo(text string) *InlineCompo { + ret := &InlinCompo{} + ret.Inline = kbd + ret.Text = text + return ret +} + // The type implements message with an inline keyboard. type InlineCompo struct { Compo @@ -39,7 +39,7 @@ type InlineCompo struct { } // Implementing the Sendable interface. -func (widget *InlineWidget) SendConfig( +func (widget *InlineCompo) SendConfig( c *Context, ) (*SendConfig) { var text string @@ -58,7 +58,7 @@ func (widget *InlineWidget) SendConfig( return ret } -// Implementing the Widget interface. +// Implementing the Server interface. func (widget *InlineCompo) Serve(c *Context) { for u := range c.Input() { var act Action @@ -91,6 +91,7 @@ func (widget *InlineCompo) Serve(c *Context) { } } +// Implementing the Filterer interface. func (compo *InlineCompo) Filter(u *Update) bool { if widget == nil || u.CallbackQuery == nil { return true diff --git a/tg/reply.go b/tg/reply.go index ce9ea78..3dc7467 100644 --- a/tg/reply.go +++ b/tg/reply.go @@ -51,8 +51,8 @@ func (kbd *Reply) ToApi() any { } // Transform the keyboard to widget with the specified text. -func (kbd *Reply) Widget(text string) *ReplyWidget { - ret := &ReplyWidget{} +func (kbd *Reply) Compo(text string) *ReplyCompo { + ret := &ReplyCompo{} ret.Reply = kbd ret.Text = text return ret @@ -112,7 +112,7 @@ func (compo *ReplyCompo) Filter( return false } -// Implementing the Widget interface. +// Implementing the UI interface. func (compo *ReplyCompo) Serve(c *Context) { for u := range c.Input() { var btn *Button diff --git a/tg/send.go b/tg/send.go index ea4606e..5379cf7 100644 --- a/tg/send.go +++ b/tg/send.go @@ -9,8 +9,10 @@ type MessageId int64 // Implementing the interface provides // way to define what message will be // sent to the side of a user. -type Renderable interface { - Render(*Context) (*SendConfig) +type Sendable interface { + SendConfig(*Context) (*SendConfig) + SetMessage(*Message) + GetMessage() *Message } type Errorer interface { diff --git a/tg/session.go b/tg/session.go index 58c965a..bfdb8f4 100644 --- a/tg/session.go +++ b/tg/session.go @@ -1,5 +1,15 @@ package tg +// The way to determine where the context is +// related to. +type SessionScope uint8 +const ( + NoSessionScope ContextScope = iota + PrivateSessionScope + GroupSessionScope + ChannelSessionScope +) + // Represents unique value to identify chats. // In fact is simply ID of the chat. type SessionId int64 @@ -14,6 +24,7 @@ func (si SessionId) ToApi() int64 { type Session struct { // Id of the chat of the user. Id SessionId + Scope SessionScope // Custom value for each user. Data any } @@ -30,8 +41,10 @@ func NewSession(id SessionId) *Session { type SessionMap map[SessionId]*Session // Add new empty session by it's ID. -func (sm SessionMap) Add(sid SessionId) { +func (sm SessionMap) Add(sid SessionId) *Session { + ret := NewSession(sid) sm[sid] = NewSession(sid) + return ret } // Session information for a group. diff --git a/tg/widget.go b/tg/widget.go index adeeb1f..6d08256 100644 --- a/tg/widget.go +++ b/tg/widget.go @@ -4,7 +4,4 @@ type Maker[V any] interface { Make(*Context) V } -type RootHandler interface { -} -