Keep implementing the new UI system.

This commit is contained in:
Andrey Parhomenko 2023-09-26 17:13:31 +03:00
parent 1f52474082
commit abf080164a
11 changed files with 145 additions and 133 deletions

View file

@ -103,34 +103,32 @@ WithInitFunc(func(c *tg.Context) {
c.Session.Data = &SessionData{} c.Session.Data = &SessionData{}
}).WithRootNode(tg.NewRootNode( }).WithRootNode(tg.NewRootNode(
// The "/" widget. // The "/" widget.
tg.WidgetFunc(func(c *tg.Context) tg.UIs { tg.WidgetFunc(func(c *tg.Context) tg.UIs {return tg.UIs{
return tg.UIs{
tg.NewKeyboard().Row( tg.NewKeyboard().Row(
tg.NewButton("GoT Github page"). tg.NewButton("GoT Github page").
WithUrl("https://github.com/mojosa-software/got"), WithUrl("https://github.com/mojosa-software/got"),
).Inline().Widget( ).Inline().Widget(
fmt.Sprintf( fmt.Sprintf(
"Hello, %s" "Hello, %s!\n"
"The testing bot started!\n", "The testing bot started!\n",
"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!",
c.SentFrom().UserName, c.SentFrom().UserName,
),
), ),
),
tg.NewKeyboard().Row( tg.NewKeyboard().Row(
tg.NewButton("Inc/Dec").Go("/inc-dec"), tg.NewButton("Inc/Dec").Go("/inc-dec"),
).Row( ).Row(
tg.NewButton("Mutate messages").Go("/mutate-messages"), tg.NewButton("Mutate messages").Go("/mutate-messages"),
).Row( ).Row(
tg.NewButton("Send location").Go("/send-location"), tg.NewButton("Send location").Go("/send-location"),
).Reply().Widget( ).Reply().Widget(
"Choose the point of your interest", "Choose the point of your interest",
), ),
} }}),
)
tg.NewNode( tg.NewNode(
"mutate-messages", tg.NewPage().WithReply( "mutate-messages", tg.NewPage().WithReply(

View file

@ -25,9 +25,9 @@ type Bot struct {
groupBehaviour *GroupBehaviour groupBehaviour *GroupBehaviour
// Bot behaviour in channels. // Bot behaviour in channels.
channelBehaviour *ChannelBehaviour channelBehaviour *ChannelBehaviour
contexts map[SessionId] *context
sessions SessionMap sessions SessionMap
groupSessions GroupSessionMap groupSessions GroupSessionMap
} }
// Return the new bot with empty sessions and behaviour. // 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. // Send the Renderable to the specified session client side.
// Can be used for both group and private sessions. // Can be used for both group and private sessions.
func (bot *Bot) Send( func (bot *Bot) Send(
sid SessionId, v Renderable, sid SessionId, v Sendable,
) (*Message, error) { ) (*Message, error) {
c, ok := bot.contexts[sid]
if !ok {
return nil, ContextNotExistErr
}
config := v.Render(sid, bot) config := v.Render(sid, bot)
if config.Error != nil { if config.Error != nil {
return nil, config.Error return nil, config.Error
@ -64,7 +69,7 @@ func (bot *Bot) Send(
return &msg, nil return &msg, nil
} }
func (bot *Bot) Render( /*func (bot *Bot) Render(
sid SessionId, r Renderable, sid SessionId, r Renderable,
) (MessageMap, error) { ) (MessageMap, error) {
configs := r.Render(sid, bot) configs := r.Render(sid, bot)
@ -84,7 +89,7 @@ func (bot *Bot) Render(
messages[config.Name] = &msg messages[config.Name] = &msg
} }
return messages, nil return messages, nil
} }*/
func (bot *Bot) GetSession( func (bot *Bot) GetSession(
sid SessionId, sid SessionId,
@ -210,57 +215,40 @@ 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) {
chans := make(map[SessionId] *UpdateChan )
var sid SessionId var sid SessionId
for u := range updates { for u := range updates {
sid = SessionId(u.FromChat().ID) sid = SessionId(u.FromChat().ID)
// Create new session if the one does not exist ctx, ctxOk := bot.contexts[sid]
// for this user. if u.Message != nil && !ctxOk {
// Create context on any message
// 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
// if we have no one. // if we have no one.
bot.sessions.Add(sid) session, sessionOk := bot.sessions[sid]
lsession := bot.sessions[sid] if !sessionOk {
ctx := &context{ // Creating session if we have none.
Bot: bot, session = bot.sessions.Add(sid)
Session: lsession,
} }
chn := NewUpdateChan() session = bot.sessions[sid]
chans[sid] = chn ctx = &context{
Bot: bot,
Session: session,
scope: PrivateContextScope,
updates: NewUpdateChan(),
}
if !ctxOk {
bot.contexts[sid] = ctx
}
go (&Context{ go (&Context{
context: ctx, context: ctx,
Update: u, Update: u,
input: chn, input: ctx.updates,
}).serve() }).serve()
ctx.updates.Send(u)
continue
} }
chn, ok := chans[sid] if ctxOk {
// The bot MUST get the "start" command. ctx.updates.Send(u)
// It will do nothing otherwise.
if ok {
chn.Send(u)
} }
} }
} }

View file

@ -40,7 +40,7 @@ func (c *Command) WithWidget(w Widget) *Command {
return c return c
} }
func (c *Command) WidgetFunc(fn WidgetFunc) *Command { func (c *Command) WidgetFunc(fn Func) *Command {
return c.WithWidget(fn) return c.WithWidget(fn)
} }
@ -195,7 +195,7 @@ func (widget *CommandWidget) Serve(c *Context) {
c.Run(cmd.Action, u) c.Run(cmd.Action, u)
if cmd.Widget != nil { if cmd.Widget != nil {
cmdUpdates.Close() cmdUpdates.Close()
cmdUpdates = c.RunWidget(cmd.Widget) cmdUpdates = c.runWidget(cmd.Widget)
} }
continue continue
} }

View file

@ -1,32 +1,34 @@
package tg 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 { type Widget interface {
UIs(*Context) UIs Render(*Context) UI
} }
// The way to describe custom function based Widgets. // The way to describe custom function based Widgets.
type WidgetFunc func(c *Context) UIs type RenderFunc func(c *Context) UI
func (fn WidgetFunc) UIs(c *Context) UIs { func (fn RenderFunc) Uis(c *Context) UI {
return fn(c) 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 // The type describes interfaces
// needed to be implemented to be endpoint handlers. // needed to be implemented to be endpoint handlers.
type UI interface { type Component interface {
Renderable // Optionaly component can implement the
// Renderable interface to automaticaly be sent to the
// user side.
SetMessage(*Message)
GetMessage() *Message
Filterer Filterer
Server Server
} }
type UiFunc func()
// The type to embed into potential components. // The type to embed into potential components.
// Implements empty versions of interfaces // Implements empty versions of interfaces
// and contains // and contains

View file

@ -7,7 +7,8 @@ import (
//"path" //"path"
) )
// General type function for faster typing. // General type function to define actions, single component widgets
// and components themselves.
type Func func(*Context) type Func func(*Context)
func (f Func) Act(c *Context) { func (f Func) Act(c *Context) {
f(c) f(c)
@ -15,16 +16,14 @@ func (f Func) Act(c *Context) {
func (f Func) Serve(c *Context) { func (f Func) Serve(c *Context) {
f(c) f(c)
} }
func(f Func) Filter(_ *Context) bool {
// The way to determine where the context is return false
// related to. }
type ContextScope uint8 func (f Func) Render(_ *Context) UI {
const ( return UI{
NoContextScope ContextScope = iota f,
PrivateContextScope }
GroupContextScope }
ChannelContextScope
)
type ContextType uint8 type ContextType uint8
const ( const (
@ -40,6 +39,8 @@ type context struct {
Session *Session Session *Session
// To reach the bot abilities inside callbacks. // To reach the bot abilities inside callbacks.
Bot *Bot Bot *Bot
Type ContextType
updates *UpdateChan
skippedUpdates *UpdateChan skippedUpdates *UpdateChan
// Current screen ID. // Current screen ID.
path, prevPath Path path, prevPath Path
@ -48,9 +49,7 @@ type context struct {
// Goroutie function to handle each user. // Goroutie function to handle each user.
func (c *Context) serve() { func (c *Context) serve() {
beh := c.Bot.behaviour beh := c.Bot.behaviour
if beh.Init != nil { c.Run(beh.Init)
c.Run(beh.Init, c.Update)
}
beh.Root.Serve(c) beh.Root.Serve(c)
} }
@ -67,7 +66,7 @@ func (c *Context) PrevPath() Path {
return c.prevPath return c.prevPath
} }
func (c *Context) Run(a Action, u *Update) { func (c *Context) Run(a Action) {
if a != nil { if a != nil {
a.Act(c.Copy().WithUpdate(u)) a.Act(c.Copy().WithUpdate(u))
} }
@ -80,12 +79,6 @@ func (c *Context) Skip(u *Update) {
c.skippedUpdates.Send(u) 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. // Sends to the Sendable object.
func (c *Context) Send(v Sendable) (*Message, error) { func (c *Context) Send(v Sendable) (*Message, error) {
return c.Bot.Send(c.Session.Id, v) return c.Bot.Send(c.Session.Id, v)
@ -134,16 +127,19 @@ func (c *Context) Copy() *Context {
} }
func (c *Context) WithArg(v any) *Context { func (c *Context) WithArg(v any) *Context {
c = c.Copy()
c.Arg = v c.Arg = v
return c return c
} }
func (c *Context) WithUpdate(u *Update) *Context { func (c *Context) WithUpdate(u *Update) *Context {
c = c.Copy()
c.Update = u c.Update = u
return c return c
} }
func (c *Context) WithInput(input *UpdateChan) *Context { func (c *Context) WithInput(input *UpdateChan) *Context {
c = c.Copy()
c.input = input c.input = input
return c return c
} }
@ -160,9 +156,6 @@ func (af ActionFunc) Act(c *Context) {
af(c) af(c)
} }
type C = Context
// Changes screen of user to the Id one. // Changes screen of user to the Id one.
func (c *Context) Go(pth Path, args ...any) error { func (c *Context) Go(pth Path, args ...any) error {
// Getting the screen and changing to // 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. // 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 { if widget == nil {
return nil return nil
} }
@ -206,11 +199,19 @@ func (c *Context) runWidget(widget Widget, args ...any) {
arg = args arg = args
} }
pth := c.Path()
uis := widget.UI() uis := widget.UI()
// Leave if changed path.
if pth != c.Path() {
return nil
}
chns := make(map[UI] *UpdateChan) chns := make(map[UI] *UpdateChan)
for _, ui := range uis { for _, ui := range uis {
msg := c.Send(ui.Render(c)) s, ok := ui.(Sendable)
ui.SetMessage(msg) if ok {
msg := c.Send(s.SendConfig(c))
ui.SetMessage(msg)
}
updates := NewUpdateChan() updates := NewUpdateChan()
go func() { go func() {
ui.Serve( ui.Serve(
@ -219,19 +220,28 @@ func (c *Context) runWidget(widget Widget, args ...any) {
WithArg(arg), WithArg(arg),
) )
// To let widgets finish themselves before // To let widgets finish themselves before
// the channel is closed. // the channel is closed and close it by themselves.
updates.Close() updates.Close()
}() }()
chns[ui] = updates chns[ui] = updates
} }
for u := range c.skippedUpdates.Chan() { ret := NewUpdateChan()
for ui := range uis { go func() {
if !ui.Filter() { for u := range ret {
chns[ui] <- u 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. // Simple way to read strings for widgets.

View file

@ -17,6 +17,7 @@ var (
EmptyKeyboardTextErr = errors.New("got empty text for a keyboard") EmptyKeyboardTextErr = errors.New("got empty text for a keyboard")
ActionNotDefinedErr = errors.New("action was not defined") ActionNotDefinedErr = errors.New("action was not defined")
MapCollisionErr = errors.New("map collision occured") MapCollisionErr = errors.New("map collision occured")
ContextNotExistErr = errors.New("the context does not exist")
) )
func (wut WrongUpdateType) Error() string { func (wut WrongUpdateType) Error() string {

View file

@ -9,14 +9,6 @@ type Inline struct {
*Keyboard *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. // 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{}
@ -31,6 +23,14 @@ func (kbd *Inline) ToApi() tgbotapi.InlineKeyboardMarkup {
return tgbotapi.NewInlineKeyboardMarkup(rows...) 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. // The type implements message with an inline keyboard.
type InlineCompo struct { type InlineCompo struct {
Compo Compo
@ -39,7 +39,7 @@ type InlineCompo struct {
} }
// Implementing the Sendable interface. // Implementing the Sendable interface.
func (widget *InlineWidget) SendConfig( func (widget *InlineCompo) SendConfig(
c *Context, c *Context,
) (*SendConfig) { ) (*SendConfig) {
var text string var text string
@ -58,7 +58,7 @@ func (widget *InlineWidget) SendConfig(
return ret return ret
} }
// Implementing the Widget interface. // Implementing the Server interface.
func (widget *InlineCompo) Serve(c *Context) { func (widget *InlineCompo) Serve(c *Context) {
for u := range c.Input() { for u := range c.Input() {
var act Action var act Action
@ -91,6 +91,7 @@ func (widget *InlineCompo) Serve(c *Context) {
} }
} }
// Implementing the Filterer interface.
func (compo *InlineCompo) Filter(u *Update) bool { func (compo *InlineCompo) Filter(u *Update) bool {
if widget == nil || u.CallbackQuery == nil { if widget == nil || u.CallbackQuery == nil {
return true return true

View file

@ -51,8 +51,8 @@ func (kbd *Reply) ToApi() any {
} }
// Transform the keyboard to widget with the specified text. // Transform the keyboard to widget with the specified text.
func (kbd *Reply) Widget(text string) *ReplyWidget { func (kbd *Reply) Compo(text string) *ReplyCompo {
ret := &ReplyWidget{} ret := &ReplyCompo{}
ret.Reply = kbd ret.Reply = kbd
ret.Text = text ret.Text = text
return ret return ret
@ -112,7 +112,7 @@ func (compo *ReplyCompo) Filter(
return false return false
} }
// Implementing the Widget interface. // Implementing the UI interface.
func (compo *ReplyCompo) Serve(c *Context) { func (compo *ReplyCompo) Serve(c *Context) {
for u := range c.Input() { for u := range c.Input() {
var btn *Button var btn *Button

View file

@ -9,8 +9,10 @@ type MessageId int64
// Implementing the interface provides // Implementing the interface provides
// 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 Renderable interface { type Sendable interface {
Render(*Context) (*SendConfig) SendConfig(*Context) (*SendConfig)
SetMessage(*Message)
GetMessage() *Message
} }
type Errorer interface { type Errorer interface {

View file

@ -1,5 +1,15 @@
package tg 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. // 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
@ -14,6 +24,7 @@ func (si SessionId) ToApi() int64 {
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
// Custom value for each user. // Custom value for each user.
Data any Data any
} }
@ -30,8 +41,10 @@ func NewSession(id SessionId) *Session {
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(sid SessionId) { func (sm SessionMap) Add(sid SessionId) *Session {
ret := NewSession(sid)
sm[sid] = NewSession(sid) sm[sid] = NewSession(sid)
return ret
} }
// Session information for a group. // Session information for a group.

View file

@ -4,7 +4,4 @@ type Maker[V any] interface {
Make(*Context) V Make(*Context) V
} }
type RootHandler interface {
}