Globlal refactoring. Less embedded structures making everything more clear.

This commit is contained in:
Andrey Parhomenko 2023-08-19 12:47:33 +03:00
parent 522b9ada02
commit 9a1202d777
9 changed files with 216 additions and 178 deletions

View file

@ -18,12 +18,12 @@ var (
incDecKeyboard = tg.NewKeyboard("").Row( incDecKeyboard = tg.NewKeyboard("").Row(
tg.NewButton("+").ActionFunc(func(c *tg.Context) { tg.NewButton("+").ActionFunc(func(c *tg.Context) {
d := c.V.(*UserData) d := c.Session.Value.(*UserData)
d.Counter++ d.Counter++
c.Sendf("%d", d.Counter) c.Sendf("%d", d.Counter)
}), }),
tg.NewButton("-").ActionFunc(func(c *tg.Context) { tg.NewButton("-").ActionFunc(func(c *tg.Context) {
d := c.SessionValue().(*UserData) d := c.Session.Value.(*UserData)
d.Counter-- d.Counter--
c.Sendf("%d", d.Counter) c.Sendf("%d", d.Counter)
}), }),
@ -50,7 +50,7 @@ var (
var err error var err error
if c.Message.Location != nil { if c.Message.Location != nil {
l := c.Message.Location l := c.Message.Location
err = c.Sendf( _, err = c.Sendf(
"Longitude: %f\n"+ "Longitude: %f\n"+
"Latitude: %f\n"+ "Latitude: %f\n"+
"Heading: %d"+ "Heading: %d"+
@ -60,7 +60,7 @@ var (
l.Heading, l.Heading,
) )
} else { } else {
err = c.Send("Somehow wrong location was sent") _, err = c.Send("Somehow wrong location was sent")
} }
if err != nil { if err != nil {
c.Send(err) c.Send(err)
@ -79,7 +79,7 @@ var (
var beh = tg.NewBehaviour(). var beh = tg.NewBehaviour().
WithInitFunc(func(c *tg.Context) { WithInitFunc(func(c *tg.Context) {
// The session initialization. // The session initialization.
c.V = &UserData{} c.Session.Value = &UserData{}
c.ChangeScreen("start") c.ChangeScreen("start")
}).WithScreens( }).WithScreens(
@ -109,7 +109,7 @@ var beh = tg.NewBehaviour().
WithKeyboard(incDecKeyboard). WithKeyboard(incDecKeyboard).
// The function will be called when reaching the screen. // The function will be called when reaching the screen.
ActionFunc(func(c *tg.Context) { ActionFunc(func(c *tg.Context) {
d := c.V.(*UserData) d := c.Session.Value.(*UserData)
c.Sendf("Current counter value = %d", d.Counter) c.Sendf("Current counter value = %d", d.Counter)
}), }),
@ -131,7 +131,7 @@ var beh = tg.NewBehaviour().
tg.NewButton("Check"). tg.NewButton("Check").
WithData("check"). WithData("check").
ActionFunc(func(a *tg.Context) { ActionFunc(func(a *tg.Context) {
d := a.V.(*UserData) d := a.Session.Value.(*UserData)
a.Sendf("Counter = %d", d.Counter) a.Sendf("Counter = %d", d.Counter)
}), }),
), ),
@ -170,7 +170,7 @@ func mutateMessage(fn func(string) string) tg.ActionFunc {
panic(err) panic(err)
} }
err = c.Sendf("%s", fn(msg)) _, err = c.Sendf("%s", fn(msg))
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -186,7 +186,7 @@ var gBeh = tg.NewGroupBehaviour().
c.Send("Hello, World!") c.Send("Hello, World!")
}), }),
tg.NewGroupCommand("mycounter").ActionFunc(func(c *tg.GC) { 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) c.Sendf("Your counter value is %d", d.Counter)
}), }),
) )
@ -200,10 +200,9 @@ func main() {
} }
bot = bot. bot = bot.
WithBehaviour(beh). WithBehaviour(beh).
WithGroupBehaviour(gBeh) WithGroupBehaviour(gBeh).
Debug(true)
bot.Debug = true log.Printf("Authorized on account %s", bot.Api.Self.UserName)
log.Printf("Authorized on account %s", bot.Self.UserName)
bot.Run() bot.Run()
} }

View file

@ -3,17 +3,19 @@ package tg
import ( import (
"errors" "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 Update = tgbotapi.Update
type Chat = apix.Chat type Chat = tgbotapi.Chat
type User = apix.User type User = tgbotapi.User
// The wrapper around Telegram API. // The wrapper around Telegram API.
type Bot struct { type Bot struct {
*apix.BotAPI Api *tgbotapi.BotAPI
Me *User Me *User
// Private bot behaviour. // Private bot behaviour.
behaviour *Behaviour behaviour *Behaviour
// Group bot behaviour. // Group bot behaviour.
@ -26,28 +28,55 @@ type Bot struct {
// Return the new bot with empty sessions and behaviour. // Return the new bot with empty sessions and behaviour.
func NewBot(token string) (*Bot, error) { func NewBot(token string) (*Bot, error) {
bot, err := apix.NewBotAPI(token) bot, err := tgbotapi.NewBotAPI(token)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Bot{ return &Bot{
BotAPI: bot, Api: bot,
}, nil }, nil
} }
func (bot *Bot) SessionValueBySid( func (bot *Bot) Debug(debug bool) *Bot {
sid SessionId, bot.Api.Debug = debug
) (any, bool) { return bot
v, ok := bot.sessions[sid]
return v.V, ok
} }
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, sid SessionId,
) (any, bool) { ) (*Session, bool) {
v, ok := bot.groupSessions[sid] session, ok := bot.sessions[sid]
return v.V, ok 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 { func (b *Bot) WithBehaviour(beh *Behaviour) *Bot {
@ -78,10 +107,9 @@ func (bot *Bot) Run() error {
bot.groupBehaviour == nil { bot.groupBehaviour == nil {
return errors.New("no behaviour defined") return errors.New("no behaviour defined")
} }
bot.Debug = true uc := tgbotapi.NewUpdate(0)
uc := apix.NewUpdate(0)
uc.Timeout = 60 uc.Timeout = 60
updates := bot.GetUpdatesChan(uc) updates := bot.Api.GetUpdatesChan(uc)
handles := make(map[string]chan *Update) handles := make(map[string]chan *Update)
if bot.behaviour != nil { if bot.behaviour != nil {
@ -97,7 +125,7 @@ func (bot *Bot) Run() error {
go bot.handleGroup(chn) go bot.handleGroup(chn)
} }
me, _ := bot.GetMe() me, _ := bot.Api.GetMe()
bot.Me = &me bot.Me = &me
for u := range updates { for u := range updates {
chn, ok := handles[u.FromChat().Type] chn, ok := handles[u.FromChat().Type]
@ -166,9 +194,9 @@ func (bot *Bot) handleGroup(updates chan *Update) {
bot.groupSessions.Add(sid) bot.groupSessions.Add(sid)
session := bot.groupSessions[sid] session := bot.groupSessions[sid]
ctx := &groupContext{ ctx := &groupContext{
Bot: bot, Bot: bot,
GroupSession: session, Session: session,
updates: make(chan *Update), updates: make(chan *Update),
} }
chn := make(chan *Update) chn := make(chan *Update)
chans[sid] = chn chans[sid] = chn

View file

@ -1,60 +1 @@
package tg 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
}

View file

@ -6,6 +6,8 @@ import (
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
"github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
type FileType int type FileType int
@ -69,3 +71,22 @@ func (f *File) UploadData() (string, io.Reader, error) {
func (f *File) SendData() string { func (f *File) SendData() string {
return "" 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
}

View file

@ -1,5 +1,11 @@
package tg package tg
import (
"fmt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
// Customized actions for the group behaviour. // Customized actions for the group behaviour.
type GroupAction interface { type GroupAction interface {
Act(*GroupContext) Act(*GroupContext)
@ -14,16 +20,70 @@ func (af GroupActionFunc) Act(a *GroupContext) {
type GC = GroupContext type GC = GroupContext
func (c *GroupContext) SentFromSid() SessionId { func (c *GroupContext) Session() *GroupSession {
return SessionId(c.SentFrom().ID) session, _ := c.Bot.GetGroupSession(
} SessionId(c.SentFrom().ID),
)
func (a *GroupContext) SessionValue() any { return session
v, _ := a.Bot.SessionValueBySid(a.SentFromSid())
return v
} }
type GroupContext struct { type GroupContext struct {
*groupContext *groupContext
*Update *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
}

View file

@ -7,8 +7,8 @@ import (
) )
type context struct { type context struct {
*Session Session *Session
*Bot Bot *Bot
updates chan *Update updates chan *Update
// Is true if currently reading the Update. // Is true if currently reading the Update.
readingUpdate bool readingUpdate bool
@ -21,7 +21,7 @@ type context struct {
// Goroutie function to handle each user. // Goroutie function to handle each user.
func (c *context) handleUpdateChan(updates chan *Update) { func (c *context) handleUpdateChan(updates chan *Update) {
beh := c.behaviour beh := c.Bot.behaviour
if beh.Init != nil { if beh.Init != nil {
c.run(beh.Init, nil) c.run(beh.Init, nil)
@ -76,7 +76,7 @@ func (c *context) handleUpdateChan(updates chan *Update) {
) )
data := u.CallbackQuery.Data data := u.CallbackQuery.Data
_, err := c.Request(cb) _, err := c.Bot.Api.Request(cb)
if err != nil { if err != nil {
panic(err) 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. // Returns the next update ignoring current screen.
func (c *context) ReadUpdate() (*Update, error) { func (c *context) ReadUpdate() (*Update, error) {
c.readingUpdate = true c.readingUpdate = true
@ -145,36 +139,17 @@ func (c *context) ReadTextMessage() (string, error) {
} }
// Sends to the user specified text. // Sends to the user specified text.
func (c *context) Send(values ...any) error { func (c *context) Send(v any) (*Message, error) {
cid := c.Id.ToTelegram() return c.Bot.Send(c.Session.Id, v)
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
} }
// 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) error { func (c *context) Sendf(format string, v ...any) (*Message, error) {
return c.Send(fmt.Sprintf(format, v...)) msg, err := c.Send(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.
@ -198,7 +173,7 @@ func (af ActionFunc) Act(c *Context) {
type ScreenChange ScreenId type ScreenChange ScreenId
func (sc ScreenChange) Act(c *Context) { func (sc ScreenChange) Act(c *Context) {
if !c.behaviour.ScreenExist(ScreenId(sc)) { if !c.Bot.behaviour.ScreenExist(ScreenId(sc)) {
panic(ScreenNotExistErr) panic(ScreenNotExistErr)
} }
err := c.ChangeScreen(ScreenId(sc)) err := c.ChangeScreen(ScreenId(sc))
@ -211,7 +186,7 @@ type C = Context
// Changes screen of user to the Id one. // Changes screen of user to the Id one.
func (c *Context) ChangeScreen(screenId ScreenId) error { func (c *Context) ChangeScreen(screenId ScreenId) error {
if !c.behaviour.ScreenExist(screenId) { if !c.Bot.behaviour.ScreenExist(screenId) {
return ScreenNotExistErr return ScreenNotExistErr
} }
@ -224,18 +199,13 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
// Getting the screen and changing to // Getting the screen and changing to
// then executing its action. // then executing its action.
screen := c.behaviour.Screens[screenId] screen := c.Bot.behaviour.Screens[screenId]
c.prevScreen = c.curScreen c.prevScreen = c.curScreen
c.curScreen = screen c.curScreen = screen
screen.Render(c.context) screen.Render(c.Session.Id, c.Bot)
if screen.Action != nil { if screen.Action != nil {
c.run(screen.Action, c.Update) c.run(screen.Action, c.Update)
} }
return nil return nil
} }
func (c *Context) SessionValue() any {
v, _ := c.SessionValueBySid(c.Id)
return v
}

View file

@ -1,7 +1,7 @@
package tg package tg
import ( 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. // 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. // Renders output of the screen only to the side of the user.
func (s *Screen) Render(c *context) error { func (s *Screen) Render(
id := c.Id.ToTelegram() sid SessionId, bot *Bot,
) ([]*Message, error) {
cid := sid.ToApi()
kbd := s.Keyboard kbd := s.Keyboard
iKbd := s.InlineKeyboard iKbd := s.InlineKeyboard
var ch [2]apix.Chattable var ch [2]tgbotapi.Chattable
var txt string var txt string
msgs := []*Message{}
// Screen text and inline keyboard. // Screen text and inline keyboard.
if s.Text != "" { if s.Text != "" {
txt = s.Text txt = s.Text
@ -82,23 +86,27 @@ func (s *Screen) Render(c *context) error {
} }
} }
if txt != "" { if txt != "" {
msg := apix.NewMessage(id, txt) msgConfig := tgbotapi.NewMessage(cid, txt)
if iKbd != nil { if iKbd != nil {
msg.ReplyMarkup = iKbd.toTelegramInline() msgConfig.ReplyMarkup = iKbd.toTelegramInline()
} else if kbd != nil { } else if kbd != nil {
msg.ReplyMarkup = kbd.toTelegramReply() msgConfig.ReplyMarkup = kbd.toTelegramReply()
if _, err := c.Bot.Send(msg); err != nil { msg, err := bot.Api.Send(msgConfig)
return err if err != nil {
return msgs, err
} }
return nil msgs = append(msgs, &msg)
return msgs, nil
} else { } else {
msg.ReplyMarkup = apix.NewRemoveKeyboard(true) msgConfig.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true)
if _, err := c.Bot.Send(msg); err != nil { msg, err := bot.Api.Send(msgConfig)
return err 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. // Screen text and reply keyboard.
@ -109,23 +117,25 @@ func (s *Screen) Render(c *context) error {
} else { } else {
txt = ">" txt = ">"
} }
msg := apix.NewMessage(id, txt) msgConfig := tgbotapi.NewMessage(cid, txt)
msg.ReplyMarkup = kbd.toTelegramReply() msgConfig.ReplyMarkup = kbd.toTelegramReply()
ch[1] = msg ch[1] = msgConfig
} else { } else {
// Removing keyboard if there is none. // Removing keyboard if there is none.
msg := apix.NewMessage(id, ">") msgConfig := tgbotapi.NewMessage(cid, ">")
msg.ReplyMarkup = apix.NewRemoveKeyboard(true) msgConfig.ReplyMarkup = tgbotapi.NewRemoveKeyboard(true)
ch[1] = msg ch[1] = msgConfig
} }
for _, m := range ch { for _, m := range ch {
if m != nil { if m != nil {
if _, err := c.Bot.Send(m); err != nil { msg, err := bot.Api.Send(m)
return err if err != nil {
return msgs, err
} }
msgs = append(msgs, &msg)
} }
} }
return nil return msgs, nil
} }

11
tg/send.go Normal file
View file

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

View file

@ -5,7 +5,7 @@ package tg
type SessionId int64 type SessionId int64
// Convert the SessionId to Telegram API's type. // Convert the SessionId to Telegram API's type.
func (si SessionId) ToTelegram() int64 { func (si SessionId) ToApi() int64 {
return int64(si) return int64(si)
} }
@ -15,14 +15,13 @@ type Session struct {
// Id of the chat of the user. // Id of the chat of the user.
Id SessionId Id SessionId
// Custom value for each user. // Custom value for each user.
V any Value any
} }
// Return new empty session with specified user ID. // Return new empty session with specified user ID.
func NewSession(id SessionId) *Session { func NewSession(id SessionId) *Session {
return &Session{ return &Session{
Id: id, Id: id,
V: make(map[string]any),
} }
} }
@ -39,14 +38,13 @@ func (sm SessionMap) Add(sid SessionId) {
type GroupSession struct { type GroupSession struct {
Id SessionId Id SessionId
// Information for each user in the group. // Information for each user in the group.
V map[SessionId]any Value any
} }
// Returns new empty group session with specified group and user IDs. // Returns new empty group session with specified group and user IDs.
func NewGroupSession(id SessionId) *GroupSession { func NewGroupSession(id SessionId) *GroupSession {
return &GroupSession{ return &GroupSession{
Id: id, Id: id,
V: make(map[SessionId]any),
} }
} }