Keep implementing the new UI system.
This commit is contained in:
parent
1f52474082
commit
abf080164a
11 changed files with 145 additions and 133 deletions
|
@ -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(
|
||||
|
|
80
tg/bot.go
80
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
26
tg/compo.go
26
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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
21
tg/inline.go
21
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -4,7 +4,4 @@ type Maker[V any] interface {
|
|||
Make(*Context) V
|
||||
}
|
||||
|
||||
type RootHandler interface {
|
||||
}
|
||||
|
||||
|
||||
|
|
Loading…
Reference in a new issue