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

View file

@ -3,16 +3,18 @@ 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
Api *tgbotapi.BotAPI
Me *User
// Private bot behaviour.
behaviour *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]
@ -167,7 +195,7 @@ func (bot *Bot) handleGroup(updates chan *Update) {
session := bot.groupSessions[sid]
ctx := &groupContext{
Bot: bot,
GroupSession: session,
Session: session,
updates: make(chan *Update),
}
chn := make(chan *Update)

View file

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

View file

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

View file

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

View file

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

View file

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

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
// 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),
}
}