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,15 +103,14 @@ 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"
"Hello, %s!\n"
"The testing bot started!\n",
"You can see the basics of usage in the ",
"cmd/test/main.go file!",
@ -129,8 +128,7 @@ WithInitFunc(func(c *tg.Context) {
"Choose the point of your interest",
),
}
)
}}),
tg.NewNode(
"mutate-messages", tg.NewPage().WithReply(

View file

@ -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
ctx, ctxOk := bot.contexts[sid]
if u.Message != nil && !ctxOk {
// Create context on any message
// if we have no one.
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{
if !sessionOk {
// Creating session if we have none.
session = bot.sessions.Add(sid)
}
session = bot.sessions[sid]
ctx = &context{
Bot: bot,
Session: session,
scope: PrivateContextScope,
updates: NewUpdateChan(),
}
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.
bot.sessions.Add(sid)
lsession := bot.sessions[sid]
ctx := &context{
Bot: bot,
Session: lsession,
}
chn := NewUpdateChan()
chans[sid] = chn
go (&Context{
context: ctx,
Update: u,
input: chn,
}).serve()
if !ctxOk {
bot.contexts[sid] = ctx
}
chn, ok := chans[sid]
// The bot MUST get the "start" command.
// It will do nothing otherwise.
if ok {
chn.Send(u)
go (&Context{
context: ctx,
Update: u,
input: ctx.updates,
}).serve()
ctx.updates.Send(u)
continue
}
if ctxOk {
ctx.updates.Send(u)
}
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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))
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() {
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.

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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.

View file

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