Making the widgets more modulable. Needs to be finished.

This commit is contained in:
Andrey Parhomenko 2023-09-16 14:34:17 +03:00
parent 5e1faf0c44
commit 57f85fdacc
15 changed files with 464 additions and 270 deletions

View file

@ -4,29 +4,33 @@ tmp_dir = "tmp"
[build] [build]
args_bin = [] args_bin = []
bin = "./exe/test.exe" bin = "exe/test.exe"
cmd = "go build -o ./exe/ ./cmd/..." cmd = "go build -o ./exe/ ./cmd/test"
delay = 0 delay = 0
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
exclude_dir = ["app/volume", "assets", "tmp", "vendor", "testdata"]
exclude_file = [] exclude_file = []
exclude_regex = ["_test.go"] exclude_regex = ["_test.go", ".exe"]
exclude_unchanged = false exclude_unchanged = false
follow_symlink = false follow_symlink = false
full_bin = "" full_bin = ""
include_dir = [] include_dir = []
include_ext = ["go", "tpl", "tmpl", "html"] include_ext = ["go", "tpl", "tmpl", "html"]
include_file = [] include_file = []
kill_delay = "0s" kill_delay = "0s"
log = "build-errors.log" log = "build-errors.log"
poll = false poll = true
poll_interval = 0 poll_interval = 0
rerun = false rerun = false
rerun_delay = 500 rerun_delay = 500
send_interrupt = false send_interrupt = true
stop_on_error = true stop_on_error = true
[color] [color]
app = "" app = "red"
build = "yellow" build = "yellow"
main = "magenta" main = "magenta"
runner = "green" runner = "green"

View file

@ -26,18 +26,21 @@ func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget {
return ret return ret
} }
func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) error { func (w *MutateMessageWidget) Serve(c *tg.Context, updates *tg.UpdateChan) {
for _, arg := range c.Args { for _, arg := range c.Args {
c.Sendf("%v", arg) c.Sendf("%v", arg)
} }
for u := range updates { for u := range updates.Chan() {
if u.Message == nil {
continue
}
text := u.Message.Text text := u.Message.Text
c.Sendf("%s", w.Mutate(text)) c.Sendf("%s", w.Mutate(text))
} }
return nil }
func (w *MutateMessageWidget) Filter(u *tg.Update, _ tg.MessageMap) bool {
if u.Message == nil {
return true
}
return false
} }
func ExtractSessionData(c *tg.Context) *SessionData { func ExtractSessionData(c *tg.Context) *SessionData {
@ -116,14 +119,14 @@ var beh = tg.NewBehaviour().
c.Session.Data = &SessionData{} c.Session.Data = &SessionData{}
}).WithScreens( }).WithScreens(
tg.NewScreen("start", tg.NewPage( tg.NewScreen("start", tg.NewPage(
"The bot started!", "",
).WithInline( ).WithInline(
tg.NewInline().Row( tg.NewInline().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"),
), ).Widget(""),
).WithReply( ).WithReply(
navKeyboard, navKeyboard.Widget("The bot started!"),
), ),
), ),
tg.NewScreen("start/inc-dec", tg.NewPage( tg.NewScreen("start/inc-dec", tg.NewPage(
@ -132,7 +135,7 @@ var beh = tg.NewBehaviour().
"by saving the counter for each of users "+ "by saving the counter for each of users "+
"separately. ", "separately. ",
).WithReply( ).WithReply(
incDecKeyboard, incDecKeyboard.Widget("Press the buttons to increment and decrement"),
).ActionFunc(func(c *tg.Context) { ).ActionFunc(func(c *tg.Context) {
// The function will be calleb before serving page. // The function will be calleb before serving page.
d := ExtractSessionData(c) d := ExtractSessionData(c)
@ -143,7 +146,7 @@ var beh = tg.NewBehaviour().
tg.NewScreen("start/upper-case", tg.NewPage( tg.NewScreen("start/upper-case", tg.NewPage(
"Type text and the bot will send you the upper case version to you", "Type text and the bot will send you the upper case version to you",
).WithReply( ).WithReply(
navToStartKeyboard, navToStartKeyboard.Widget(""),
).WithSub( ).WithSub(
NewMutateMessageWidget(strings.ToUpper), NewMutateMessageWidget(strings.ToUpper),
), ),
@ -152,7 +155,7 @@ var beh = tg.NewBehaviour().
tg.NewScreen("start/lower-case", tg.NewPage( tg.NewScreen("start/lower-case", tg.NewPage(
"Type text and the bot will send you the lower case version", "Type text and the bot will send you the lower case version",
).WithReply( ).WithReply(
navToStartKeyboard, navToStartKeyboard.Widget(""),
).WithSub( ).WithSub(
NewMutateMessageWidget(strings.ToLower), NewMutateMessageWidget(strings.ToLower),
), ),
@ -161,7 +164,7 @@ var beh = tg.NewBehaviour().
tg.NewScreen("start/send-location", tg.NewPage( tg.NewScreen("start/send-location", tg.NewPage(
"Send your location and I will tell where you are!", "Send your location and I will tell where you are!",
).WithReply( ).WithReply(
sendLocationKeyboard, sendLocationKeyboard.Widget(""),
).WithInline( ).WithInline(
tg.NewInline().Row( tg.NewInline().Row(
tg.NewButton( tg.NewButton(
@ -172,7 +175,7 @@ var beh = tg.NewBehaviour().
d := ExtractSessionData(c) d := ExtractSessionData(c)
c.Sendf("Counter = %d", d.Counter) c.Sendf("Counter = %d", d.Counter)
}), }),
), ).Widget("Press the button to display your counter"),
), ),
), ),
).WithCommands( ).WithCommands(
@ -189,9 +192,9 @@ var beh = tg.NewBehaviour().
}), }),
tg.NewCommand("read"). tg.NewCommand("read").
Desc("reads a string and sends it back"). Desc("reads a string and sends it back").
WidgetFunc(func(c *tg.Context, updates chan *tg.Update) error { WidgetFunc(func(c *tg.Context, updates *tg.UpdateChan) {
c.Sendf("Type text and I will send it back to you") c.Sendf("Type text and I will send it back to you")
for u := range updates { for u := range updates.Chan() {
if u.Message == nil { if u.Message == nil {
continue continue
} }
@ -199,7 +202,6 @@ var beh = tg.NewBehaviour().
break break
} }
c.Sendf("Done") c.Sendf("Done")
return nil
}), }),
tg.NewCommand("image"). tg.NewCommand("image").
Desc("sends a sample image"). Desc("sends a sample image").

View file

@ -50,9 +50,9 @@ func (bot *Bot) Debug(debug bool) *Bot {
func (bot *Bot) Send( func (bot *Bot) Send(
sid SessionId, v Sendable, sid SessionId, v Sendable,
) (*Message, error) { ) (*Message, error) {
config, err := v.SendConfig(sid, bot) config := v.SendConfig(sid, bot)
if err != nil { if config.Error != nil {
return nil, err return nil, config.Error
} }
msg, err := bot.Api.Send(config.ToApi()) msg, err := bot.Api.Send(config.ToApi())
@ -64,18 +64,22 @@ func (bot *Bot) Send(
func (bot *Bot) Render( func (bot *Bot) Render(
sid SessionId, r Renderable, sid SessionId, r Renderable,
) ([]*Message, error) { ) (MessageMap, error) {
configs, err := r.Render(sid, bot) configs := r.Render(sid, bot)
if err != nil { if configs == nil {
return []*Message{}, err return nil, MapCollisionErr
} }
messages := []*Message{} messages := make(MessageMap)
for _, config := range configs { for _, config := range configs {
_, collision := messages[config.Name]
if collision {
return messages, MapCollisionErr
}
msg, err := bot.Api.Send(config.ToApi()) msg, err := bot.Api.Send(config.ToApi())
if err != nil { if err != nil {
return messages, err return messages, err
} }
messages = append(messages, &msg) messages[config.Name] = &msg
} }
return messages, nil return messages, nil
} }
@ -204,7 +208,7 @@ 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]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)
@ -224,9 +228,12 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
Bot: bot, Bot: bot,
Session: session, Session: session,
} }
chn := make(chan *Update) chn := NewUpdateChan()
chans[sid] = chn chans[sid] = chn
go ctx.handleUpdateChan(chn) go (&Context{
context: ctx,
Update: u,
}).Serve(chn)
} }
} else if u.Message != nil { } else if u.Message != nil {
// Create session on any message // Create session on any message
@ -237,16 +244,19 @@ func (bot *Bot) handlePrivate(updates chan *Update) {
Bot: bot, Bot: bot,
Session: lsession, Session: lsession,
} }
chn := make(chan *Update) chn := NewUpdateChan()
chans[sid] = chn chans[sid] = chn
go ctx.handleUpdateChan(chn) go (&Context{
context: ctx,
Update: u,
}).Serve(chn)
} }
chn, ok := chans[sid] chn, ok := chans[sid]
// The bot MUST get the "start" command. // The bot MUST get the "start" command.
// It will do nothing otherwise. // It will do nothing otherwise.
if ok { if ok {
chn <- u chn.Send(u)
} }
} }
} }

View file

@ -142,7 +142,21 @@ func (w *CommandWidget) WithUsageFunc(fn ActionFunc) *CommandWidget {
return w.WithUsage(fn) return w.WithUsage(fn)
} }
func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error { func (widget *Command) Filter(
u *Update,
msgs ...*Message,
) bool {
/*if u.Message == nil || !u.Message.IsCommand() {
return false
}*/
return false
}
func (widget *CommandWidget) Serve(
c *Context,
updates *UpdateChan,
) {
commanders := make(map[CommandName] BotCommander) commanders := make(map[CommandName] BotCommander)
for k, v := range widget.Commands { for k, v := range widget.Commands {
commanders[k] = v commanders[k] = v
@ -153,7 +167,7 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
) )
var cmdUpdates chan *Update var cmdUpdates chan *Update
for u := range updates { for u := range updates.Chan() {
if c.ScreenId() == "" && u.Message != nil { if c.ScreenId() == "" && 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.
@ -178,13 +192,12 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
if cmdUpdates != nil { if cmdUpdates != nil {
close(cmdUpdates) close(cmdUpdates)
} }
cmdUpdates = make(chan *Update) cmdUpdates := NewUpdateChan()
go func() { go func() {
cmd.Widget.Serve( cmd.Widget.Serve(
&Context{context: c.context, Update: u}, &Context{context: c.context, Update: u},
cmdUpdates, cmdUpdates,
) )
close(cmdUpdates)
cmdUpdates = nil cmdUpdates = nil
}() }()
} }
@ -199,5 +212,4 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error {
c.Skip(u) c.Skip(u)
} }
} }
return nil
} }

View file

@ -16,6 +16,7 @@ var (
NotAvailableErr = errors.New("the context is not available") NotAvailableErr = errors.New("the context is not available")
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")
) )
func (wut WrongUpdateType) Error() string { func (wut WrongUpdateType) Error() string {

View file

@ -74,7 +74,7 @@ func (f *File) SendData() string {
} }
func (f *File) SendConfig( func (f *File) SendConfig(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) (*SendConfig, error) { ) (*SendConfig) {
var config SendConfig var config SendConfig
cid := sid.ToApi() cid := sid.ToApi()
@ -85,9 +85,9 @@ func (f *File) SendConfig(
config.Image = &photo config.Image = &photo
default: default:
return nil, UnknownFileTypeErr panic(UnknownFileTypeErr)
} }
return &config, nil return &config
} }

View file

@ -83,13 +83,9 @@ func (c *groupContext) Sendf(
format string, format string,
v ...any, v ...any,
) (*Message, error) { ) (*Message, error) {
msg, err := c.Send(NewMessage( return c.Send(NewMessage(
c.Session.Id, fmt.Sprintf(format, v...), fmt.Sprintf(format, v...),
)) ))
if err != nil {
return nil, err
}
return msg, err
} }
// Sends into the chat specified values converted to strings. // Sends into the chat specified values converted to strings.

View file

@ -10,6 +10,7 @@ type Keyboard struct {
// defined action for the button. // defined action for the button.
Action *action Action *action
Rows []ButtonRow Rows []ButtonRow
buttonMap ButtonMap
} }
// The type represents reply keyboards. // The type represents reply keyboards.
@ -59,6 +60,14 @@ func (kbd *InlineKeyboard) ActionFunc(fn ActionFunc) *InlineKeyboard {
return kbd.WithAction(fn) return kbd.WithAction(fn)
} }
// Transform the keyboard to widget with the specified text.
func (kbd *InlineKeyboard) Widget(text string) *InlineKeyboardWidget {
ret := &InlineKeyboardWidget{}
ret.InlineKeyboard = kbd
ret.Text = text
return ret
}
// Adds a new button row to the current keyboard. // Adds a new button row to the current keyboard.
func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard { func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard {
// For empty row. We do not need that. // For empty row. We do not need that.
@ -80,6 +89,13 @@ func (kbd *ReplyKeyboard) ActionFunc(fn ActionFunc) *ReplyKeyboard {
return kbd.WithAction(fn) return kbd.WithAction(fn)
} }
func (kbd *ReplyKeyboard) Widget(text string) *ReplyKeyboardWidget {
ret := &ReplyKeyboardWidget{}
ret.ReplyKeyboard = kbd
ret.Text = text
return ret
}
// Convert the Keyboard to the Telegram API type of reply keyboard. // Convert the Keyboard to the Telegram API type of reply keyboard.
func (kbd *ReplyKeyboard) ToApi() any { func (kbd *ReplyKeyboard) ToApi() any {
// Shades everything. // Shades everything.
@ -133,12 +149,16 @@ func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard {
// Returns the map of buttons. Used to define the Action. // Returns the map of buttons. Used to define the Action.
func (kbd Keyboard) ButtonMap() ButtonMap { func (kbd Keyboard) ButtonMap() ButtonMap {
if kbd.buttonMap != nil {
return kbd.buttonMap
}
ret := make(ButtonMap) ret := make(ButtonMap)
for _, vi := range kbd.Rows { for _, vi := range kbd.Rows {
for _, vj := range vi { for _, vj := range vi {
ret[vj.Key()] = vj ret[vj.Key()] = vj
} }
} }
kbd.buttonMap = ret
return ret return ret
} }

View file

@ -4,48 +4,23 @@ import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
// Simple text message type.
type MessageConfig struct { type MessageConfig struct {
To SessionId
ReplyTo MessageId
Text string Text string
Inline *InlineKeyboard
Reply *ReplyKeyboard
} }
func NewMessage(to SessionId, text string) *MessageConfig { // Return new message with the specified text.
func NewMessage(text string) *MessageConfig {
ret := &MessageConfig{} ret := &MessageConfig{}
ret.To = to
ret.Text = text ret.Text = text
return ret return ret
} }
func (config *MessageConfig) WithInline(
inline *InlineKeyboard,
) *MessageConfig {
config.Inline = inline
return config
}
func (config *MessageConfig) WithReply(
reply *ReplyKeyboard,
) *MessageConfig {
config.Reply = reply
return config
}
func (config *MessageConfig) SendConfig( func (config *MessageConfig) SendConfig(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) (*SendConfig, error) { ) (*SendConfig) {
var ret SendConfig var ret SendConfig
msg := tgbotapi.NewMessage(config.To.ToApi(), config.Text) msg := tgbotapi.NewMessage(sid.ToApi(), config.Text)
if config.Inline != nil {
msg.ReplyMarkup = config.Inline.ToApi()
}
// Reply shades the inline.
if config.Reply != nil {
msg.ReplyMarkup = config.Reply.ToApi()
}
ret.Message = &msg ret.Message = &msg
return &ret, nil return &ret
} }

View file

@ -1,7 +1,7 @@
package tg package tg
import ( import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" //tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
// The basic widget to provide keyboard functionality // The basic widget to provide keyboard functionality
@ -9,9 +9,9 @@ import (
type Page struct { type Page struct {
Text string Text string
SubWidget Widget SubWidget Widget
Inline *InlineKeyboard Inline *InlineKeyboardWidget
Reply *ReplyKeyboard
Action Action Action Action
Reply *ReplyKeyboardWidget
} }
// Return new page with the specified text. // Return new page with the specified text.
@ -22,13 +22,13 @@ func NewPage(text string) *Page {
} }
// Set the inline keyboard. // Set the inline keyboard.
func (p *Page) WithInline(inline *InlineKeyboard) *Page { func (p *Page) WithInline(inline *InlineKeyboardWidget) *Page {
p.Inline = inline p.Inline = inline
return p return p
} }
// Set the reply keyboard. // Set the reply keyboard.
func (p *Page) WithReply(reply *ReplyKeyboard) *Page { func (p *Page) WithReply(reply *ReplyKeyboardWidget) *Page {
p.Reply = reply p.Reply = reply
return p return p
} }
@ -51,150 +51,76 @@ func (p *Page) WithSub(sub Widget) *Page {
return p return p
} }
func (p *Page) Serve(
c *Context, updates chan *Update,
) error {
msgs, err := c.Render(p)
if err != nil {
return err
}
// The inline message is always returned func (p *Page) Render(
// and the reply one is useless in our case.
inlineMsg := msgs[0]
if p.Action != nil {
c.run(p.Action, c.Update)
}
var subUpdates chan *Update
if p.SubWidget != nil {
subUpdates = make(chan *Update)
go p.SubWidget.Serve(c, subUpdates)
defer close(subUpdates)
}
for u := range updates {
var act Action
if u.Message != nil {
text := u.Message.Text
kbd := p.Reply
if kbd == nil {
if subUpdates != nil {
subUpdates <- u
}
continue
}
btns := kbd.ButtonMap()
btn, ok := btns[text]
if !ok {
if u.Message.Location != nil {
for _, b := range btns {
if b.SendLocation {
btn = b
ok = true
}
}
} else if subUpdates != nil {
subUpdates <- u
}
}
if btn != nil {
act = btn.Action
} else if kbd.Action != nil {
act = kbd.Action
}
} else if u.CallbackQuery != nil {
if u.CallbackQuery.Message.MessageID != inlineMsg.MessageID {
if subUpdates != nil {
subUpdates <- u
}
continue
}
cb := tgbotapi.NewCallback(
u.CallbackQuery.ID,
u.CallbackQuery.Data,
)
data := u.CallbackQuery.Data
_, err := c.Bot.Api.Request(cb)
if err != nil {
return err
}
kbd := p.Inline
if kbd == nil {
if subUpdates != nil {
subUpdates <- u
}
continue
}
btns := kbd.ButtonMap()
btn, ok := btns[data]
if !ok {
if subUpdates != nil {
subUpdates <- u
}
continue
}
if btn != nil {
act = btn.Action
} else if kbd.Action != nil {
act = kbd.Action
}
}
c.Run(act, u)
}
return nil
}
func (s *Page) Render(
sid SessionId, bot *Bot, sid SessionId, bot *Bot,
) ([]*SendConfig, error) { ) ([]*SendConfig) {
cid := sid.ToApi() reply := p.Reply
reply := s.Reply inline := p.Inline
inline := s.Inline
ret := []*SendConfig{} ret := []*SendConfig{}
var txt string
// Screen text and inline keyboard. if p.Text != "" {
if s.Text != "" { cfg := NewMessage(p.Text).SendConfig(sid, bot).
txt = s.Text WithName("page/text")
} else if inline != nil { ret = append(ret, cfg)
// Default to send the keyboard.
txt = ">"
} }
if txt != "" { if inline != nil {
msgConfig := tgbotapi.NewMessage(cid, txt) cfg := inline.SendConfig(sid, bot).
if inline != nil { WithName("page/inline")
msgConfig.ReplyMarkup = inline.ToApi() ret = append(ret, cfg)
} else if reply != nil { }
msgConfig.ReplyMarkup = reply.ToApi() if p.Reply != nil {
ret = append(ret, &SendConfig{Message: &msgConfig}) cfg := reply.SendConfig(sid, bot).
return ret, nil WithName("page/reply")
} else { ret = append(ret, cfg)
msgConfig.ReplyMarkup = NewReply().
WithRemove(true).
ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
return ret, nil
}
ret = append(ret, &SendConfig{Message: &msgConfig})
} }
// Screen text and reply keyboard. return ret
if reply != nil {
msgConfig := tgbotapi.NewMessage(cid, ">")
msgConfig.ReplyMarkup = reply.ToApi()
ret = append(ret, &SendConfig{
Message: &msgConfig,
})
} else {
// Removing keyboard if there is none.
msgConfig := tgbotapi.NewMessage(cid, ">")
msgConfig.ReplyMarkup = NewReply().
WithRemove(true).
ToApi()
ret = append(ret, &SendConfig{Message: &msgConfig})
}
return ret, nil
} }
func (p *Page) Filter(
u *Update, msgs MessageMap,
) bool {
return false
}
func (p *Page) Serve(
c *Context, updates *UpdateChan,
) {
msgs, _ := c.Render(p)
inlineMsg := msgs["page/inline"]
if p.Action != nil {
c.Run(p.Action, c.Update)
}
subUpdates := c.RunWidgetBg(p.SubWidget)
defer subUpdates.Close()
inlineUpdates := c.RunWidgetBg(p.Inline)
defer inlineUpdates.Close()
replyUpdates := c.RunWidgetBg(p.Reply)
defer replyUpdates.Close()
subFilter, subFilterOk := p.SubWidget.(Filterer)
for u := range updates.Chan() {
switch {
case !p.Inline.Filter(u, MessageMap{"": inlineMsg}) :
inlineUpdates.Send(u)
case !p.Reply.Filter(u, msgs) :
replyUpdates.Send(u )
case p.SubWidget != nil :
if subFilterOk {
if subFilter.Filter(u, msgs) {
subUpdates.Send(u)
}
} else {
subUpdates.Send(u)
}
default:
}
}
}

View file

@ -10,7 +10,7 @@ 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
skippedUpdates chan *Update skippedUpdates *UpdateChan
// Current screen ID. // Current screen ID.
screenId, prevScreenId ScreenId screenId, prevScreenId ScreenId
} }
@ -20,14 +20,12 @@ type context struct {
// handling functions. Is provided to Act() function always. // handling functions. Is provided to Act() function always.
// Goroutie function to handle each user. // Goroutie function to handle each user.
func (c *context) handleUpdateChan(updates chan *Update) { func (c *Context) Serve(updates *UpdateChan) {
beh := c.Bot.behaviour beh := c.Bot.behaviour
if beh.Init != nil { if beh.Init != nil {
c.run(beh.Init, nil) c.Run(beh.Init, c.Update)
} }
beh.Root.Serve(&Context{ beh.Root.Serve(c, updates)
context: c,
}, updates)
} }
@ -53,14 +51,12 @@ func (c *Context) Run(a Action, u *Update) {
// Skip the update sending it down to // Skip the update sending it down to
// the underlying widget. // the underlying widget.
func (c *Context) Skip(u *Update) { func (c *Context) Skip(u *Update) {
if c.skippedUpdates != nil { c.skippedUpdates.Send(u)
c.skippedUpdates <- u
}
} }
// Renders the Renedrable object to the side of client // Renders the Renedrable object to the side of client
// and returns the messages it sent. // and returns the messages it sent.
func (c *Context) Render(v Renderable) ([]*Message, error) { func (c *Context) Render(v Renderable) (MessageMap, error) {
return c.Bot.Render(c.Session.Id, v) return c.Bot.Render(c.Session.Id, v)
} }
@ -71,13 +67,7 @@ func (c *Context) Send(v Sendable) (*Message, error) {
// Sends the formatted with fmt.Sprintf message to the user. // Sends the formatted with fmt.Sprintf message to the user.
func (c *Context) Sendf(format string, v ...any) (*Message, error) { func (c *Context) Sendf(format string, v ...any) (*Message, error) {
msg, err := c.Send(NewMessage( return c.Send(NewMessage(fmt.Sprintf(format, v...)))
c.Session.Id, fmt.Sprintf(format, v...),
))
if err != nil {
return nil, err
}
return msg, err
} }
// Interface to interact with the user. // Interface to interact with the user.
@ -130,10 +120,8 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
c.screenId = screenId c.screenId = screenId
// Making the new channel for the widget. // Making the new channel for the widget.
if c.skippedUpdates != nil { c.skippedUpdates.Close()
close(c.skippedUpdates) c.skippedUpdates = NewUpdateChan()
}
c.skippedUpdates = make(chan *Update)
if screen.Widget != nil { if screen.Widget != nil {
// Running the widget if the screen has one. // Running the widget if the screen has one.
go func() { go func() {
@ -142,6 +130,8 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
Update: c.Update, Update: c.Update,
Args: args, Args: args,
}, c.skippedUpdates) }, c.skippedUpdates)
c.skippedUpdates.Close()
}() }()
} else { } else {
panic("no widget defined for the screen") panic("no widget defined for the screen")
@ -149,3 +139,7 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error {
return nil return nil
} }
func (c *Context) ChangeToPrevScreen() {
c.ChangeScreen(c.PrevScreenId())
}

View file

@ -11,9 +11,6 @@ type Screen struct {
Id ScreenId Id ScreenId
// The widget to run when reaching the screen. // The widget to run when reaching the screen.
Widget Widget Widget Widget
// Needs implementation later.
Dynamic DynamicWidget
} }
// Map structure for the screens. // Map structure for the screens.
@ -27,8 +24,3 @@ func NewScreen(id ScreenId, widget Widget) *Screen {
} }
} }
func (s *Screen) WithDynamic(dynamic DynamicWidget) *Screen {
s.Dynamic = dynamic
return s
}

View file

@ -10,25 +10,38 @@ type Image any
// Implementing the interface lets the // Implementing the interface lets the
// value to be sent. // value to be sent.
type Sendable interface { type Sendable interface {
SendConfig(SessionId, *Bot) (*SendConfig, error) SendConfig(SessionId, *Bot) *SendConfig
} }
type Renderable interface { type Renderable interface {
Render(SessionId, *Bot) ([]*SendConfig, error) Render(SessionId, *Bot) ([]*SendConfig)
}
type Errorer interface {
Err() error
} }
// The type is used as an endpoint to send messages // The type is used as an endpoint to send messages
// via bot.Send . // via bot.Send .
type SendConfig struct { type SendConfig struct {
// Simple message with text. // The name will be used to store
// to add text use lower image // the message in the map.
// or see the ParseMode for tgbotapi . Name string
// Message with text and keyboard.
Message *tgbotapi.MessageConfig Message *tgbotapi.MessageConfig
// The image to be sent. // The image to be sent.
Image *tgbotapi.PhotoConfig Image *tgbotapi.PhotoConfig
Error error
} }
func (cfg *SendConfig) WithName(name string) *SendConfig {
cfg.Name = name
return cfg
}
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 {
if config.Message != nil { if config.Message != nil {

View file

@ -14,9 +14,6 @@ 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
// True if the session started.
// (got the '/start' command.
started bool
// Custom value for each user. // Custom value for each user.
Data any Data any
} }

View file

@ -1,7 +1,7 @@
package tg package tg
import ( import (
//tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
// Implementing the interface provides // Implementing the interface provides
@ -12,7 +12,80 @@ type Widget interface {
// widget MUST end its work. // widget MUST end its work.
// Mostly made by looping over the // Mostly made by looping over the
// updates range. // updates range.
Serve(*Context, chan *Update) error Serve(*Context, *UpdateChan)
}
// Needs implementation.
// Behaviour can be the root widget or something like
// that.
type RootWidget interface {
Widget
}
// Implementing the interface provides way
// to know exactly what kind of updates
// the widget needs.
type Filterer interface {
// Return true if should filter the update
// and not send it inside the widget.
Filter(*Update, MessageMap) bool
}
// The type represents general update channel.
type UpdateChan struct {
chn chan *Update
}
// Return new update channel.
func NewUpdateChan() *UpdateChan {
ret := &UpdateChan{}
ret.chn = make(chan *Update)
return ret
}
func (updates *UpdateChan) Chan() chan *Update {
return updates.chn
}
// Send an update to the channel.
func (updates *UpdateChan) Send(u *Update) {
if updates != nil && updates.chn == nil {
return
}
updates.chn <- u
}
// Read an update from the channel.
func (updates *UpdateChan) Read() *Update {
if updates == nil || updates.chn == nil {
return nil
}
return <-updates.chn
}
// Returns true if the channel is closed.
func (updates *UpdateChan) Closed() bool {
return updates.chn == nil
}
// Close the channel. Used in defers.
func (updates *UpdateChan) Close() {
if updates == nil || updates.chn == nil {
return
}
close(updates.chn)
updates.chn = nil
}
func (c *Context) RunWidgetBg(widget Widget) *UpdateChan {
if widget == nil {
return nil
}
updates := NewUpdateChan()
go widget.Serve(c, updates)
return updates
} }
// Implementing the interface provides // Implementing the interface provides
@ -22,9 +95,188 @@ type DynamicWidget interface {
// The function that implements the Widget // The function that implements the Widget
// interface. // interface.
type WidgetFunc func(*Context, chan *Update) error type WidgetFunc func(*Context, *UpdateChan)
func (wf WidgetFunc) Serve(c *Context, updates chan *Update) error { func (wf WidgetFunc) Serve(c *Context, updates *UpdateChan) {
return wf(c, updates) wf(c, updates)
}
func (wf WidgetFunc) Filter(
u *Update,
msgs ...*Message,
) bool {
return false
}
// The type implements message with an inline keyboard.
type InlineKeyboardWidget struct {
Text string
*InlineKeyboard
}
// The type implements dynamic inline keyboard widget.
// Aka message with inline keyboard.
func NewInlineKeyboardWidget(
text string,
inline *InlineKeyboard,
) *InlineKeyboardWidget {
ret := &InlineKeyboardWidget{}
ret.Text = text
ret.InlineKeyboard = inline
return ret
}
func (widget *InlineKeyboardWidget) SendConfig(
sid SessionId,
bot *Bot,
) (*SendConfig) {
var text string
if widget.Text != "" {
text = widget.Text
} else {
text = ">"
}
msgConfig := tgbotapi.NewMessage(sid.ToApi(), text)
msgConfig.ReplyMarkup = widget.ToApi()
ret := &SendConfig{}
ret.Message = &msgConfig
return ret
}
func (widget *InlineKeyboardWidget) Serve(
c *Context,
updates *UpdateChan,
) {
for u := range updates.Chan() {
var act Action
if u.CallbackQuery == nil {
continue
}
cb := tgbotapi.NewCallback(
u.CallbackQuery.ID,
u.CallbackQuery.Data,
)
data := u.CallbackQuery.Data
_, err := c.Bot.Api.Request(cb)
if err != nil {
//return err
continue
}
btns := widget.ButtonMap()
btn, ok := btns[data]
if !ok {
continue
}
if btn != nil {
act = btn.Action
} else if widget.Action != nil {
act = widget.Action
}
c.Run(act, u)
}
}
func (widget *InlineKeyboardWidget) Filter(
u *Update,
msgs MessageMap,
) bool {
if widget == nil {
return true
}
if u.CallbackQuery == nil || len(msgs) < 1 {
return true
}
inlineMsg, inlineOk := msgs[""]
if inlineOk {
if u.CallbackQuery.Message.MessageID !=
inlineMsg.MessageID {
return true
}
}
return false
}
// The type implements dynamic reply keyboard widget.
type ReplyKeyboardWidget struct {
Text string
*ReplyKeyboard
}
// Returns new empty reply keyboard widget.
func NewReplyKeyboardWidget(
text string,
reply *ReplyKeyboard,
) *ReplyKeyboardWidget {
ret := &ReplyKeyboardWidget{}
ret.Text = text
ret.ReplyKeyboard = reply
return ret
}
func (widget *ReplyKeyboardWidget) SendConfig(
sid SessionId,
bot *Bot,
) (*SendConfig) {
var text string
if widget.Text != "" {
text = widget.Text
} else {
text = ">"
}
msgConfig := tgbotapi.NewMessage(sid.ToApi(), text)
msgConfig.ReplyMarkup = widget.ToApi()
ret := &SendConfig{}
ret.Message = &msgConfig
return ret
}
func (widget *ReplyKeyboardWidget) Filter(
u *Update,
msgs MessageMap,
) bool {
if widget == nil {
return true
}
if u.Message == nil {
return true
}
_, ok := widget.ButtonMap()[u.Message.Text]
if !ok {
return true
}
return false
}
func (widget *ReplyKeyboardWidget) Serve(
c *Context,
updates *UpdateChan,
) {
for u := range updates.Chan() {
var btn *Button
text := u.Message.Text
btns := widget.ButtonMap()
btn, ok := btns[text]
if !ok {
if u.Message.Location != nil {
for _, b := range btns {
if b.SendLocation {
btn = b
ok = true
}
}
}
}
if btn != nil {
c.Run(btn.Action, u)
}
}
} }