diff --git a/airfile b/.air.toml similarity index 72% rename from airfile rename to .air.toml index aefa582..f20e344 100644 --- a/airfile +++ b/.air.toml @@ -4,29 +4,33 @@ tmp_dir = "tmp" [build] args_bin = [] - bin = "./exe/test.exe" - cmd = "go build -o ./exe/ ./cmd/..." + bin = "exe/test.exe" + cmd = "go build -o ./exe/ ./cmd/test" delay = 0 - exclude_dir = ["assets", "tmp", "vendor", "testdata"] + + exclude_dir = ["app/volume", "assets", "tmp", "vendor", "testdata"] exclude_file = [] - exclude_regex = ["_test.go"] + exclude_regex = ["_test.go", ".exe"] + exclude_unchanged = false follow_symlink = false full_bin = "" + include_dir = [] include_ext = ["go", "tpl", "tmpl", "html"] include_file = [] + kill_delay = "0s" log = "build-errors.log" - poll = false + poll = true poll_interval = 0 rerun = false rerun_delay = 500 - send_interrupt = false + send_interrupt = true stop_on_error = true [color] - app = "" + app = "red" build = "yellow" main = "magenta" runner = "green" diff --git a/cmd/test/main.go b/cmd/test/main.go index 7b82e9e..d4fb134 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -26,18 +26,21 @@ func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget { 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 { c.Sendf("%v", arg) } - for u := range updates { - if u.Message == nil { - continue - } + for u := range updates.Chan() { text := u.Message.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 { @@ -116,14 +119,14 @@ var beh = tg.NewBehaviour(). c.Session.Data = &SessionData{} }).WithScreens( tg.NewScreen("start", tg.NewPage( - "The bot started!", + "", ).WithInline( tg.NewInline().Row( tg.NewButton("GoT Github page"). WithUrl("https://github.com/mojosa-software/got"), - ), + ).Widget(""), ).WithReply( - navKeyboard, + navKeyboard.Widget("The bot started!"), ), ), tg.NewScreen("start/inc-dec", tg.NewPage( @@ -132,7 +135,7 @@ var beh = tg.NewBehaviour(). "by saving the counter for each of users "+ "separately. ", ).WithReply( - incDecKeyboard, + incDecKeyboard.Widget("Press the buttons to increment and decrement"), ).ActionFunc(func(c *tg.Context) { // The function will be calleb before serving page. d := ExtractSessionData(c) @@ -143,7 +146,7 @@ var beh = tg.NewBehaviour(). tg.NewScreen("start/upper-case", tg.NewPage( "Type text and the bot will send you the upper case version to you", ).WithReply( - navToStartKeyboard, + navToStartKeyboard.Widget(""), ).WithSub( NewMutateMessageWidget(strings.ToUpper), ), @@ -152,7 +155,7 @@ var beh = tg.NewBehaviour(). tg.NewScreen("start/lower-case", tg.NewPage( "Type text and the bot will send you the lower case version", ).WithReply( - navToStartKeyboard, + navToStartKeyboard.Widget(""), ).WithSub( NewMutateMessageWidget(strings.ToLower), ), @@ -161,7 +164,7 @@ var beh = tg.NewBehaviour(). tg.NewScreen("start/send-location", tg.NewPage( "Send your location and I will tell where you are!", ).WithReply( - sendLocationKeyboard, + sendLocationKeyboard.Widget(""), ).WithInline( tg.NewInline().Row( tg.NewButton( @@ -172,7 +175,7 @@ var beh = tg.NewBehaviour(). d := ExtractSessionData(c) c.Sendf("Counter = %d", d.Counter) }), - ), + ).Widget("Press the button to display your counter"), ), ), ).WithCommands( @@ -189,9 +192,9 @@ var beh = tg.NewBehaviour(). }), tg.NewCommand("read"). 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") - for u := range updates { + for u := range updates.Chan() { if u.Message == nil { continue } @@ -199,7 +202,6 @@ var beh = tg.NewBehaviour(). break } c.Sendf("Done") - return nil }), tg.NewCommand("image"). Desc("sends a sample image"). diff --git a/tg/bot.go b/tg/bot.go index 2fe226d..a48cfae 100644 --- a/tg/bot.go +++ b/tg/bot.go @@ -50,9 +50,9 @@ func (bot *Bot) Debug(debug bool) *Bot { func (bot *Bot) Send( sid SessionId, v Sendable, ) (*Message, error) { - config, err := v.SendConfig(sid, bot) - if err != nil { - return nil, err + config := v.SendConfig(sid, bot) + if config.Error != nil { + return nil, config.Error } msg, err := bot.Api.Send(config.ToApi()) @@ -64,18 +64,22 @@ func (bot *Bot) Send( func (bot *Bot) Render( sid SessionId, r Renderable, -) ([]*Message, error) { - configs, err := r.Render(sid, bot) - if err != nil { - return []*Message{}, err +) (MessageMap, error) { + configs := r.Render(sid, bot) + if configs == nil { + return nil, MapCollisionErr } - messages := []*Message{} + messages := make(MessageMap) for _, config := range configs { + _, collision := messages[config.Name] + if collision { + return messages, MapCollisionErr + } msg, err := bot.Api.Send(config.ToApi()) if err != nil { return messages, err } - messages = append(messages, &msg) + messages[config.Name] = &msg } return messages, nil } @@ -204,7 +208,7 @@ 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]chan *Update) + chans := make(map[SessionId] *UpdateChan ) var sid SessionId for u := range updates { sid = SessionId(u.FromChat().ID) @@ -224,9 +228,12 @@ func (bot *Bot) handlePrivate(updates chan *Update) { Bot: bot, Session: session, } - chn := make(chan *Update) + chn := NewUpdateChan() chans[sid] = chn - go ctx.handleUpdateChan(chn) + go (&Context{ + context: ctx, + Update: u, + }).Serve(chn) } } else if u.Message != nil { // Create session on any message @@ -237,16 +244,19 @@ func (bot *Bot) handlePrivate(updates chan *Update) { Bot: bot, Session: lsession, } - chn := make(chan *Update) + chn := NewUpdateChan() chans[sid] = chn - go ctx.handleUpdateChan(chn) + go (&Context{ + context: ctx, + Update: u, + }).Serve(chn) } chn, ok := chans[sid] // The bot MUST get the "start" command. // It will do nothing otherwise. if ok { - chn <- u + chn.Send(u) } } } diff --git a/tg/command.go b/tg/command.go index 1ba5ce1..a0defec 100644 --- a/tg/command.go +++ b/tg/command.go @@ -142,7 +142,21 @@ func (w *CommandWidget) WithUsageFunc(fn ActionFunc) *CommandWidget { 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) for k, v := range widget.Commands { commanders[k] = v @@ -153,7 +167,7 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error { ) var cmdUpdates chan *Update - for u := range updates { + for u := range updates.Chan() { if c.ScreenId() == "" && u.Message != nil { // Skipping and executing the preinit action // while we have the empty screen. @@ -178,13 +192,12 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error { if cmdUpdates != nil { close(cmdUpdates) } - cmdUpdates = make(chan *Update) + cmdUpdates := NewUpdateChan() go func() { cmd.Widget.Serve( &Context{context: c.context, Update: u}, cmdUpdates, ) - close(cmdUpdates) cmdUpdates = nil }() } @@ -199,5 +212,4 @@ func (widget *CommandWidget) Serve(c *Context, updates chan *Update) error { c.Skip(u) } } - return nil } diff --git a/tg/errors.go b/tg/errors.go index 2923381..f3caf0e 100644 --- a/tg/errors.go +++ b/tg/errors.go @@ -16,6 +16,7 @@ var ( NotAvailableErr = errors.New("the context is not available") EmptyKeyboardTextErr = errors.New("got empty text for a keyboard") ActionNotDefinedErr = errors.New("action was not defined") + MapCollisionErr = errors.New("map collision occured") ) func (wut WrongUpdateType) Error() string { diff --git a/tg/file.go b/tg/file.go index 95b2dd8..9ea91ef 100644 --- a/tg/file.go +++ b/tg/file.go @@ -74,7 +74,7 @@ func (f *File) SendData() string { } func (f *File) SendConfig( sid SessionId, bot *Bot, -) (*SendConfig, error) { +) (*SendConfig) { var config SendConfig cid := sid.ToApi() @@ -85,9 +85,9 @@ func (f *File) SendConfig( config.Image = &photo default: - return nil, UnknownFileTypeErr + panic(UnknownFileTypeErr) } - return &config, nil + return &config } diff --git a/tg/group.go b/tg/group.go index 47ade58..f49b843 100644 --- a/tg/group.go +++ b/tg/group.go @@ -83,13 +83,9 @@ func (c *groupContext) Sendf( format string, v ...any, ) (*Message, error) { - msg, err := c.Send(NewMessage( - c.Session.Id, fmt.Sprintf(format, v...), + return c.Send(NewMessage( + fmt.Sprintf(format, v...), )) - if err != nil { - return nil, err - } - return msg, err } // Sends into the chat specified values converted to strings. diff --git a/tg/keyboard.go b/tg/keyboard.go index 6a09b56..0bd579d 100644 --- a/tg/keyboard.go +++ b/tg/keyboard.go @@ -10,6 +10,7 @@ type Keyboard struct { // defined action for the button. Action *action Rows []ButtonRow + buttonMap ButtonMap } // The type represents reply keyboards. @@ -59,6 +60,14 @@ func (kbd *InlineKeyboard) ActionFunc(fn ActionFunc) *InlineKeyboard { 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. func (kbd *ReplyKeyboard) Row(btns ...*Button) *ReplyKeyboard { // For empty row. We do not need that. @@ -80,6 +89,13 @@ func (kbd *ReplyKeyboard) ActionFunc(fn ActionFunc) *ReplyKeyboard { 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. func (kbd *ReplyKeyboard) ToApi() any { // Shades everything. @@ -133,12 +149,16 @@ func (kbd *ReplyKeyboard) WithOneTime(oneTime bool) *ReplyKeyboard { // Returns the map of buttons. Used to define the Action. func (kbd Keyboard) ButtonMap() ButtonMap { + if kbd.buttonMap != nil { + return kbd.buttonMap + } ret := make(ButtonMap) for _, vi := range kbd.Rows { for _, vj := range vi { ret[vj.Key()] = vj } } + kbd.buttonMap = ret return ret } diff --git a/tg/message.go b/tg/message.go index 0fd1d20..55cd0ab 100644 --- a/tg/message.go +++ b/tg/message.go @@ -4,48 +4,23 @@ import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) +// Simple text message type. type MessageConfig struct { - To SessionId - ReplyTo MessageId 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.To = to ret.Text = text 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( sid SessionId, bot *Bot, -) (*SendConfig, error) { +) (*SendConfig) { var ret SendConfig - msg := tgbotapi.NewMessage(config.To.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() - } - + msg := tgbotapi.NewMessage(sid.ToApi(), config.Text) ret.Message = &msg - return &ret, nil + return &ret } diff --git a/tg/page.go b/tg/page.go index d584edc..99c6aad 100644 --- a/tg/page.go +++ b/tg/page.go @@ -1,7 +1,7 @@ package tg 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 @@ -9,9 +9,9 @@ import ( type Page struct { Text string SubWidget Widget - Inline *InlineKeyboard - Reply *ReplyKeyboard + Inline *InlineKeyboardWidget Action Action + Reply *ReplyKeyboardWidget } // Return new page with the specified text. @@ -22,13 +22,13 @@ func NewPage(text string) *Page { } // Set the inline keyboard. -func (p *Page) WithInline(inline *InlineKeyboard) *Page { +func (p *Page) WithInline(inline *InlineKeyboardWidget) *Page { p.Inline = inline return p } // Set the reply keyboard. -func (p *Page) WithReply(reply *ReplyKeyboard) *Page { +func (p *Page) WithReply(reply *ReplyKeyboardWidget) *Page { p.Reply = reply return p } @@ -51,150 +51,76 @@ func (p *Page) WithSub(sub Widget) *Page { 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 - // 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( +func (p *Page) Render( sid SessionId, bot *Bot, -) ([]*SendConfig, error) { - cid := sid.ToApi() - reply := s.Reply - inline := s.Inline +) ([]*SendConfig) { + reply := p.Reply + inline := p.Inline + ret := []*SendConfig{} - var txt string - // Screen text and inline keyboard. - if s.Text != "" { - txt = s.Text - } else if inline != nil { - // Default to send the keyboard. - txt = ">" + + if p.Text != "" { + cfg := NewMessage(p.Text).SendConfig(sid, bot). + WithName("page/text") + ret = append(ret, cfg) } - if txt != "" { - msgConfig := tgbotapi.NewMessage(cid, txt) - if inline != nil { - msgConfig.ReplyMarkup = inline.ToApi() - } else if reply != nil { - msgConfig.ReplyMarkup = reply.ToApi() - ret = append(ret, &SendConfig{Message: &msgConfig}) - return ret, nil - } else { - msgConfig.ReplyMarkup = NewReply(). - WithRemove(true). - ToApi() - ret = append(ret, &SendConfig{Message: &msgConfig}) - return ret, nil - } - ret = append(ret, &SendConfig{Message: &msgConfig}) + if inline != nil { + cfg := inline.SendConfig(sid, bot). + WithName("page/inline") + ret = append(ret, cfg) + } + if p.Reply != nil { + cfg := reply.SendConfig(sid, bot). + WithName("page/reply") + ret = append(ret, cfg) } - // Screen text and reply keyboard. - 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 + return ret } +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: + } + } +} + + diff --git a/tg/private.go b/tg/private.go index 251cf09..4970ac3 100644 --- a/tg/private.go +++ b/tg/private.go @@ -10,7 +10,7 @@ type context struct { Session *Session // To reach the bot abilities inside callbacks. Bot *Bot - skippedUpdates chan *Update + skippedUpdates *UpdateChan // Current screen ID. screenId, prevScreenId ScreenId } @@ -20,14 +20,12 @@ type context struct { // handling functions. Is provided to Act() function always. // Goroutie function to handle each user. -func (c *context) handleUpdateChan(updates chan *Update) { +func (c *Context) Serve(updates *UpdateChan) { beh := c.Bot.behaviour if beh.Init != nil { - c.run(beh.Init, nil) + c.Run(beh.Init, c.Update) } - beh.Root.Serve(&Context{ - context: c, - }, updates) + beh.Root.Serve(c, updates) } @@ -53,14 +51,12 @@ func (c *Context) Run(a Action, u *Update) { // Skip the update sending it down to // the underlying widget. func (c *Context) Skip(u *Update) { - if c.skippedUpdates != nil { - c.skippedUpdates <- 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) ([]*Message, error) { +func (c *Context) Render(v Renderable) (MessageMap, error) { 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. func (c *Context) Sendf(format string, v ...any) (*Message, error) { - msg, err := c.Send(NewMessage( - c.Session.Id, fmt.Sprintf(format, v...), - )) - if err != nil { - return nil, err - } - return msg, err + return c.Send(NewMessage(fmt.Sprintf(format, v...))) } // Interface to interact with the user. @@ -130,10 +120,8 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error { c.screenId = screenId // Making the new channel for the widget. - if c.skippedUpdates != nil { - close(c.skippedUpdates) - } - c.skippedUpdates = make(chan *Update) + c.skippedUpdates.Close() + c.skippedUpdates = NewUpdateChan() if screen.Widget != nil { // Running the widget if the screen has one. go func() { @@ -142,6 +130,8 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error { Update: c.Update, Args: args, }, c.skippedUpdates) + + c.skippedUpdates.Close() }() } else { panic("no widget defined for the screen") @@ -149,3 +139,7 @@ func (c *Context) ChangeScreen(screenId ScreenId, args ...any) error { return nil } + +func (c *Context) ChangeToPrevScreen() { + c.ChangeScreen(c.PrevScreenId()) +} diff --git a/tg/screen.go b/tg/screen.go index 496293a..a740c6e 100644 --- a/tg/screen.go +++ b/tg/screen.go @@ -11,9 +11,6 @@ type Screen struct { Id ScreenId // The widget to run when reaching the screen. Widget Widget - - // Needs implementation later. - Dynamic DynamicWidget } // 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 -} - diff --git a/tg/send.go b/tg/send.go index f659ffa..13e1819 100644 --- a/tg/send.go +++ b/tg/send.go @@ -10,25 +10,38 @@ type Image any // Implementing the interface lets the // value to be sent. type Sendable interface { - SendConfig(SessionId, *Bot) (*SendConfig, error) + SendConfig(SessionId, *Bot) *SendConfig } 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 // via bot.Send . type SendConfig struct { - // Simple message with text. - // to add text use lower image - // or see the ParseMode for tgbotapi . + // The name will be used to store + // the message in the map. + Name string + // Message with text and keyboard. Message *tgbotapi.MessageConfig // The image to be sent. 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. func (config *SendConfig) ToApi() tgbotapi.Chattable { if config.Message != nil { diff --git a/tg/session.go b/tg/session.go index 917e660..58c965a 100644 --- a/tg/session.go +++ b/tg/session.go @@ -14,9 +14,6 @@ func (si SessionId) ToApi() int64 { type Session struct { // Id of the chat of the user. Id SessionId - // True if the session started. - // (got the '/start' command. - started bool // Custom value for each user. Data any } diff --git a/tg/widget.go b/tg/widget.go index 133674b..0339f94 100644 --- a/tg/widget.go +++ b/tg/widget.go @@ -1,7 +1,7 @@ package tg 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 @@ -12,7 +12,80 @@ type Widget interface { // widget MUST end its work. // Mostly made by looping over the // 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 @@ -22,9 +95,188 @@ type DynamicWidget interface { // The function that implements the Widget // interface. -type WidgetFunc func(*Context, chan *Update) error +type WidgetFunc func(*Context, *UpdateChan) -func (wf WidgetFunc) Serve(c *Context, updates chan *Update) error { - return wf(c, updates) +func (wf WidgetFunc) Serve(c *Context, updates *UpdateChan) { + 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) + } + } }