diff --git a/cmd/test/main.go b/cmd/test/main.go index 9410a79..3c8548a 100644 --- a/cmd/test/main.go +++ b/cmd/test/main.go @@ -18,12 +18,12 @@ var ( incDecKeyboard = tg.NewKeyboard("").Row( tg.NewButton("+").ActionFunc(func(c *tg.Context) { - d := c.V.(*UserData) + d := c.Session.Value.(*UserData) d.Counter++ c.Sendf("%d", d.Counter) }), tg.NewButton("-").ActionFunc(func(c *tg.Context) { - d := c.SessionValue().(*UserData) + d := c.Session.Value.(*UserData) d.Counter-- c.Sendf("%d", d.Counter) }), @@ -50,7 +50,7 @@ var ( var err error if c.Message.Location != nil { l := c.Message.Location - err = c.Sendf( + _, err = c.Sendf( "Longitude: %f\n"+ "Latitude: %f\n"+ "Heading: %d"+ @@ -60,7 +60,7 @@ var ( l.Heading, ) } else { - err = c.Send("Somehow wrong location was sent") + _, err = c.Send("Somehow wrong location was sent") } if err != nil { c.Send(err) @@ -79,7 +79,7 @@ var ( var beh = tg.NewBehaviour(). WithInitFunc(func(c *tg.Context) { // The session initialization. - c.V = &UserData{} + c.Session.Value = &UserData{} c.ChangeScreen("start") }).WithScreens( @@ -109,7 +109,7 @@ var beh = tg.NewBehaviour(). WithKeyboard(incDecKeyboard). // The function will be called when reaching the screen. ActionFunc(func(c *tg.Context) { - d := c.V.(*UserData) + d := c.Session.Value.(*UserData) c.Sendf("Current counter value = %d", d.Counter) }), @@ -131,7 +131,7 @@ var beh = tg.NewBehaviour(). tg.NewButton("Check"). WithData("check"). ActionFunc(func(a *tg.Context) { - d := a.V.(*UserData) + d := a.Session.Value.(*UserData) a.Sendf("Counter = %d", d.Counter) }), ), @@ -170,7 +170,7 @@ func mutateMessage(fn func(string) string) tg.ActionFunc { panic(err) } - err = c.Sendf("%s", fn(msg)) + _, err = c.Sendf("%s", fn(msg)) if err != nil { panic(err) } @@ -186,7 +186,7 @@ var gBeh = tg.NewGroupBehaviour(). c.Send("Hello, World!") }), tg.NewGroupCommand("mycounter").ActionFunc(func(c *tg.GC) { - d := c.SessionValue().(*UserData) + d := c.Session().Value.(*UserData) c.Sendf("Your counter value is %d", d.Counter) }), ) @@ -200,10 +200,9 @@ func main() { } bot = bot. WithBehaviour(beh). - WithGroupBehaviour(gBeh) + WithGroupBehaviour(gBeh). + Debug(true) - bot.Debug = true - - log.Printf("Authorized on account %s", bot.Self.UserName) + log.Printf("Authorized on account %s", bot.Api.Self.UserName) bot.Run() } diff --git a/tg/bot.go b/tg/bot.go index 18ff4d4..1bf0a05 100644 --- a/tg/bot.go +++ b/tg/bot.go @@ -3,17 +3,19 @@ package tg import ( "errors" - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" + "fmt" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) -type Update = apix.Update -type Chat = apix.Chat -type User = apix.User +type Update = tgbotapi.Update +type Chat = tgbotapi.Chat +type User = tgbotapi.User // The wrapper around Telegram API. type Bot struct { - *apix.BotAPI - Me *User + Api *tgbotapi.BotAPI + Me *User // Private bot behaviour. behaviour *Behaviour // Group bot behaviour. @@ -26,28 +28,55 @@ type Bot struct { // Return the new bot with empty sessions and behaviour. func NewBot(token string) (*Bot, error) { - bot, err := apix.NewBotAPI(token) + bot, err := tgbotapi.NewBotAPI(token) if err != nil { return nil, err } return &Bot{ - BotAPI: bot, + Api: bot, }, nil } -func (bot *Bot) SessionValueBySid( - sid SessionId, -) (any, bool) { - v, ok := bot.sessions[sid] - return v.V, ok +func (bot *Bot) Debug(debug bool) *Bot { + bot.Api.Debug = debug + return bot } -func (bot *Bot) GetGroupSessionValue( +func (bot *Bot) Send( + sid SessionId, v any, +) (*Message, error) { + sendable, ok := v.(Sendable) + if !ok { + cid := sid.ToApi() + str := tgbotapi.NewMessage( + cid, fmt.Sprint(v), + ) + msg, err := bot.Api.Send(str) + return &msg, err + } + + return sendable.Send(sid, bot) +} + +func (bot *Bot) Render( + sid SessionId, r Renderable, +) ([]*Message, error) { + return r.Render(sid, bot) +} + +func (bot *Bot) GetSession( sid SessionId, -) (any, bool) { - v, ok := bot.groupSessions[sid] - return v.V, ok +) (*Session, bool) { + session, ok := bot.sessions[sid] + return session, ok +} + +func (bot *Bot) GetGroupSession( + sid SessionId, +) (*GroupSession, bool) { + session, ok := bot.groupSessions[sid] + return session, ok } func (b *Bot) WithBehaviour(beh *Behaviour) *Bot { @@ -78,10 +107,9 @@ func (bot *Bot) Run() error { bot.groupBehaviour == nil { return errors.New("no behaviour defined") } - bot.Debug = true - uc := apix.NewUpdate(0) + uc := tgbotapi.NewUpdate(0) uc.Timeout = 60 - updates := bot.GetUpdatesChan(uc) + updates := bot.Api.GetUpdatesChan(uc) handles := make(map[string]chan *Update) if bot.behaviour != nil { @@ -97,7 +125,7 @@ func (bot *Bot) Run() error { go bot.handleGroup(chn) } - me, _ := bot.GetMe() + me, _ := bot.Api.GetMe() bot.Me = &me for u := range updates { chn, ok := handles[u.FromChat().Type] @@ -166,9 +194,9 @@ func (bot *Bot) handleGroup(updates chan *Update) { bot.groupSessions.Add(sid) session := bot.groupSessions[sid] ctx := &groupContext{ - Bot: bot, - GroupSession: session, - updates: make(chan *Update), + Bot: bot, + Session: session, + updates: make(chan *Update), } chn := make(chan *Update) chans[sid] = chn diff --git a/tg/context.go b/tg/context.go index ee408c8..4c5b505 100644 --- a/tg/context.go +++ b/tg/context.go @@ -1,60 +1 @@ package tg - -import ( - "fmt" - - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" -) - -// Context for interaction inside groups. -type groupContext struct { - *GroupSession - *Bot - updates chan *Update -} - -func (c *groupContext) run(a GroupAction, u *Update) { - go a.Act(&GroupContext{ - groupContext: c, - Update: u, - }) -} - -func (c *groupContext) handleUpdateChan(updates chan *Update) { - var act GroupAction - beh := c.groupBehaviour - for u := range updates { - if u.Message != nil { - msg := u.Message - if msg.IsCommand() { - cmdName := CommandName(msg.Command()) - - // Skipping the commands sent not to us. - atName := msg.CommandWithAt()[len(cmdName)+1:] - if c.Bot.Me.UserName != atName { - continue - } - cmd, ok := beh.Commands[cmdName] - if !ok { - // Some lack of command handling - continue - } - act = cmd.Action - } - } - if act != nil { - c.run(act, u) - } - } -} - -func (c *groupContext) Sendf(format string, v ...any) error { - return c.Send(fmt.Sprintf(format, v...)) -} - -// Sends into the chat specified values converted to strings. -func (c *groupContext) Send(v ...any) error { - msg := apix.NewMessage(c.Id.ToTelegram(), fmt.Sprint(v...)) - _, err := c.Bot.Send(msg) - return err -} diff --git a/tg/file.go b/tg/file.go index 238c9a2..4f15ecf 100644 --- a/tg/file.go +++ b/tg/file.go @@ -6,6 +6,8 @@ import ( "io" "os" "path/filepath" + + "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) type FileType int @@ -69,3 +71,22 @@ func (f *File) UploadData() (string, io.Reader, error) { func (f *File) SendData() string { return "" } +func (f *File) Send( + sid SessionId, bot *Bot, +) (*Message, error) { + var chattable tgbotapi.Chattable + cid := sid.ToApi() + + switch f.Type() { + case ImageFileType: + photo := tgbotapi.NewPhoto(cid, f) + photo.Caption = f.caption + chattable = photo + default: + return nil, UnknownFileTypeErr + } + + msg, err := bot.Api.Send(chattable) + + return &msg, err +} diff --git a/tg/group.go b/tg/group.go index a47f401..54b61fe 100644 --- a/tg/group.go +++ b/tg/group.go @@ -1,5 +1,11 @@ package tg +import ( + "fmt" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + // Customized actions for the group behaviour. type GroupAction interface { Act(*GroupContext) @@ -14,16 +20,70 @@ func (af GroupActionFunc) Act(a *GroupContext) { type GC = GroupContext -func (c *GroupContext) SentFromSid() SessionId { - return SessionId(c.SentFrom().ID) -} - -func (a *GroupContext) SessionValue() any { - v, _ := a.Bot.SessionValueBySid(a.SentFromSid()) - return v +func (c *GroupContext) Session() *GroupSession { + session, _ := c.Bot.GetGroupSession( + SessionId(c.SentFrom().ID), + ) + return session } type GroupContext struct { *groupContext *Update } + +// Context for interaction inside groups. +type groupContext struct { + Session *GroupSession + Bot *Bot + updates chan *Update +} + +func (c *groupContext) run(a GroupAction, u *Update) { + go a.Act(&GroupContext{ + groupContext: c, + Update: u, + }) +} + +func (c *groupContext) handleUpdateChan(updates chan *Update) { + var act GroupAction + beh := c.Bot.groupBehaviour + for u := range updates { + if u.Message != nil { + msg := u.Message + if msg.IsCommand() { + cmdName := CommandName(msg.Command()) + + // Skipping the commands sent not to us. + atName := msg.CommandWithAt()[len(cmdName)+1:] + if c.Bot.Me.UserName != atName { + continue + } + cmd, ok := beh.Commands[cmdName] + if !ok { + // Some lack of command handling + continue + } + act = cmd.Action + } + } + if act != nil { + c.run(act, u) + } + } +} + +func (c *groupContext) Sendf(format string, v ...any) error { + return c.Send(fmt.Sprintf(format, v...)) +} + +// Sends into the chat specified values converted to strings. +func (c *groupContext) Send(v ...any) error { + msg := tgbotapi.NewMessage( + c.Session.Id.ToApi(), + fmt.Sprint(v...), + ) + _, err := c.Bot.Api.Send(msg) + return err +} diff --git a/tg/private.go b/tg/private.go index 4cba9f1..96ec730 100644 --- a/tg/private.go +++ b/tg/private.go @@ -7,8 +7,8 @@ import ( ) type context struct { - *Session - *Bot + Session *Session + Bot *Bot updates chan *Update // Is true if currently reading the Update. readingUpdate bool @@ -21,7 +21,7 @@ type context struct { // Goroutie function to handle each user. func (c *context) handleUpdateChan(updates chan *Update) { - beh := c.behaviour + beh := c.Bot.behaviour if beh.Init != nil { c.run(beh.Init, nil) @@ -76,7 +76,7 @@ func (c *context) handleUpdateChan(updates chan *Update) { ) data := u.CallbackQuery.Data - _, err := c.Request(cb) + _, err := c.Bot.Api.Request(cb) if err != nil { panic(err) } @@ -113,12 +113,6 @@ func (c *context) run(a Action, u *Update) { }) } -func (c *context) SendFile(f *File) error { - switch f.typ { - } - return nil -} - // Returns the next update ignoring current screen. func (c *context) ReadUpdate() (*Update, error) { c.readingUpdate = true @@ -145,36 +139,17 @@ func (c *context) ReadTextMessage() (string, error) { } // Sends to the user specified text. -func (c *context) Send(values ...any) error { - cid := c.Id.ToTelegram() - for _, v := range values { - var msg tgbotapi.Chattable - - switch rv := v.(type) { - case *File: - switch rv.Type() { - case ImageFileType: - msg = tgbotapi.NewPhoto(cid, rv) - default: - return UnknownFileTypeErr - } - default: - msg = tgbotapi.NewMessage( - cid, fmt.Sprint(v), - ) - } - - _, err := c.Bot.Send(msg) - if err != nil { - return err - } - } - return nil +func (c *context) Send(v any) (*Message, error) { + return c.Bot.Send(c.Session.Id, v) } // Sends the formatted with fmt.Sprintf message to the user. -func (c *context) Sendf(format string, v ...any) error { - return c.Send(fmt.Sprintf(format, v...)) +func (c *context) Sendf(format string, v ...any) (*Message, error) { + msg, err := c.Send(fmt.Sprintf(format, v...)) + if err != nil { + return nil, err + } + return msg, err } // Interface to interact with the user. @@ -198,7 +173,7 @@ func (af ActionFunc) Act(c *Context) { type ScreenChange ScreenId func (sc ScreenChange) Act(c *Context) { - if !c.behaviour.ScreenExist(ScreenId(sc)) { + if !c.Bot.behaviour.ScreenExist(ScreenId(sc)) { panic(ScreenNotExistErr) } err := c.ChangeScreen(ScreenId(sc)) @@ -211,7 +186,7 @@ type C = Context // Changes screen of user to the Id one. func (c *Context) ChangeScreen(screenId ScreenId) error { - if !c.behaviour.ScreenExist(screenId) { + if !c.Bot.behaviour.ScreenExist(screenId) { return ScreenNotExistErr } @@ -224,18 +199,13 @@ func (c *Context) ChangeScreen(screenId ScreenId) error { // Getting the screen and changing to // then executing its action. - screen := c.behaviour.Screens[screenId] + screen := c.Bot.behaviour.Screens[screenId] c.prevScreen = c.curScreen c.curScreen = screen - screen.Render(c.context) + screen.Render(c.Session.Id, c.Bot) if screen.Action != nil { c.run(screen.Action, c.Update) } return nil } - -func (c *Context) SessionValue() any { - v, _ := c.SessionValueBySid(c.Id) - return v -} diff --git a/tg/screen.go b/tg/screen.go index 792553a..8a34e32 100644 --- a/tg/screen.go +++ b/tg/screen.go @@ -1,7 +1,7 @@ package tg import ( - apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) // Unique identifier for the screen. @@ -62,14 +62,18 @@ func (s *Screen) ActionFunc(a ActionFunc) *Screen { } // Renders output of the screen only to the side of the user. -func (s *Screen) Render(c *context) error { - id := c.Id.ToTelegram() +func (s *Screen) Render( + sid SessionId, bot *Bot, +) ([]*Message, error) { + cid := sid.ToApi() kbd := s.Keyboard iKbd := s.InlineKeyboard - var ch [2]apix.Chattable + var ch [2]tgbotapi.Chattable var txt string + msgs := []*Message{} + // Screen text and inline keyboard. if s.Text != "" { txt = s.Text @@ -82,23 +86,27 @@ func (s *Screen) Render(c *context) error { } } if txt != "" { - msg := apix.NewMessage(id, txt) + msgConfig := tgbotapi.NewMessage(cid, txt) if iKbd != nil { - msg.ReplyMarkup = iKbd.toTelegramInline() + msgConfig.ReplyMarkup = iKbd.toTelegramInline() } else if kbd != nil { - msg.ReplyMarkup = kbd.toTelegramReply() - if _, err := c.Bot.Send(msg); err != nil { - return err + msgConfig.ReplyMarkup = kbd.toTelegramReply() + msg, err := bot.Api.Send(msgConfig) + if err != nil { + return msgs, err } - return nil + msgs = append(msgs, &msg) + return msgs, nil } else { - msg.ReplyMarkup = apix.NewRemoveKeyboard(true) - if _, err := c.Bot.Send(msg); err != nil { - return err + msgConfig.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true) + msg, err := bot.Api.Send(msgConfig) + if err != nil { + return msgs, err } - return nil + msgs = append(msgs, &msg) + return msgs, nil } - ch[0] = msg + ch[0] = msgConfig } // Screen text and reply keyboard. @@ -109,23 +117,25 @@ func (s *Screen) Render(c *context) error { } else { txt = ">" } - msg := apix.NewMessage(id, txt) - msg.ReplyMarkup = kbd.toTelegramReply() - ch[1] = msg + msgConfig := tgbotapi.NewMessage(cid, txt) + msgConfig.ReplyMarkup = kbd.toTelegramReply() + ch[1] = msgConfig } else { // Removing keyboard if there is none. - msg := apix.NewMessage(id, ">") - msg.ReplyMarkup = apix.NewRemoveKeyboard(true) - ch[1] = msg + msgConfig := tgbotapi.NewMessage(cid, ">") + msgConfig.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true) + ch[1] = msgConfig } for _, m := range ch { if m != nil { - if _, err := c.Bot.Send(m); err != nil { - return err + msg, err := bot.Api.Send(m) + if err != nil { + return msgs, err } + msgs = append(msgs, &msg) } } - return nil + return msgs, nil } diff --git a/tg/send.go b/tg/send.go new file mode 100644 index 0000000..e6c84e3 --- /dev/null +++ b/tg/send.go @@ -0,0 +1,11 @@ +package tg + +// Implementing the interface lets the +// value to be sent. +type Sendable interface { + Send(SessionId, *Bot) (*Message, error) +} + +type Renderable interface { + Render(SessionId, *Bot) ([]*Message, error) +} diff --git a/tg/session.go b/tg/session.go index 7dcb1e5..5161376 100644 --- a/tg/session.go +++ b/tg/session.go @@ -5,7 +5,7 @@ package tg type SessionId int64 // Convert the SessionId to Telegram API's type. -func (si SessionId) ToTelegram() int64 { +func (si SessionId) ToApi() int64 { return int64(si) } @@ -15,14 +15,13 @@ type Session struct { // Id of the chat of the user. Id SessionId // Custom value for each user. - V any + Value any } // Return new empty session with specified user ID. func NewSession(id SessionId) *Session { return &Session{ Id: id, - V: make(map[string]any), } } @@ -39,14 +38,13 @@ func (sm SessionMap) Add(sid SessionId) { type GroupSession struct { Id SessionId // Information for each user in the group. - V map[SessionId]any + Value any } // Returns new empty group session with specified group and user IDs. func NewGroupSession(id SessionId) *GroupSession { return &GroupSession{ Id: id, - V: make(map[SessionId]any), } }