Now can automatically set the commands on the user side.

This commit is contained in:
Andrey Parhomenko 2023-09-11 13:00:38 +03:00
parent 221df4b0ba
commit 6c6b041133
7 changed files with 168 additions and 78 deletions

View file

@ -26,7 +26,7 @@ func NewMutateMessageWidget(fn func(string) string) *MutateMessageWidget {
return ret return ret
} }
func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) { func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) error {
for u := range updates { for u := range updates {
if u.Message == nil { if u.Message == nil {
continue continue
@ -34,6 +34,7 @@ func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) {
text := u.Message.Text text := u.Message.Text
c.Sendf("%s", w.Mutate(text)) c.Sendf("%s", w.Mutate(text))
} }
return nil
} }
func ExtractSessionData(c *tg.Context) *SessionData { func ExtractSessionData(c *tg.Context) *SessionData {
@ -173,47 +174,47 @@ var beh = tg.NewBehaviour().
), ),
), ),
), ),
).WithCommands( ).WithCommands(
tg.NewCommand("start"). tg.NewCommand("start").
Desc("start or restart the bot or move to the start screen"). Desc("start or restart the bot or move to the start screen").
ActionFunc(func(c *tg.Context){ ActionFunc(func(c *tg.Context){
c.ChangeScreen("start") c.ChangeScreen("start")
}), }),
tg.NewCommand("hello"). tg.NewCommand("hello").
Desc("sends the 'Hello, World!' message back"). Desc("sends the 'Hello, World!' message back").
ActionFunc(func(c *tg.Context) { ActionFunc(func(c *tg.Context) {
c.Sendf("Hello, World!") c.Sendf("Hello, World!")
}), }),
tg.NewCommand("read"). tg.NewCommand("read").
Desc("reads a string and sends it back"). Desc("reads a string and sends it back").
ActionFunc(func(c *tg.Context) { ActionFunc(func(c *tg.Context) {
/*c.Sendf("Type some text:") /*c.Sendf("Type some text:")
msg, err := c.ReadTextMessage() msg, err := c.ReadTextMessage()
if err != nil { if err != nil {
return return
} }
c.Sendf("You typed %q", msg)*/ c.Sendf("You typed %q", msg)*/
}), }),
tg.NewCommand("image"). tg.NewCommand("image").
Desc("sends a sample image"). Desc("sends a sample image").
ActionFunc(func(c *tg.Context) { ActionFunc(func(c *tg.Context) {
img := tg.NewFile("media/cat.jpg").Image().Caption("A cat!") img := tg.NewFile("media/cat.jpg").Image().Caption("A cat!")
c.Send(img) c.Send(img)
}), }),
tg.NewCommand("botname"). tg.NewCommand("botname").
Desc("get the bot name"). Desc("get the bot name").
ActionFunc(func(c *tg.Context) { ActionFunc(func(c *tg.Context) {
bd := c.Bot.Value().(*BotData) bd := c.Bot.Data.(*BotData)
c.Sendf("My name is %q", bd.Name) c.Sendf("My name is %q", bd.Name)
}), }),
) )
var gBeh = tg.NewGroupBehaviour(). var gBeh = tg.NewGroupBehaviour().
InitFunc(func(c *tg.GC) { InitFunc(func(c *tg.GC) {
}). }).
WithCommands( WithCommands(
tg.NewGroupCommand("hello").ActionFunc(func(c *tg.GC) { tg.NewGroupCommand("hello").ActionFunc(func(c *tg.GC) {
c.Send("Hello, World!") c.Sendf("Hello, World!")
}), }),
tg.NewGroupCommand("mycounter").ActionFunc(func(c *tg.GC) { tg.NewGroupCommand("mycounter").ActionFunc(func(c *tg.GC) {
d := c.Session().Data.(*SessionData) d := c.Session().Data.(*SessionData)
@ -231,11 +232,15 @@ func main() {
bot = bot. bot = bot.
WithBehaviour(beh). WithBehaviour(beh).
WithGroupBehaviour(gBeh). WithGroupBehaviour(gBeh).
WithValue(&BotData{
Name: "Jay",
}).
Debug(true) Debug(true)
bot.Data = &BotData{
Name: "Jay",
}
log.Printf("Authorized on account %s", bot.Api.Self.UserName) log.Printf("Authorized on account %s", bot.Api.Self.UserName)
bot.Run() err = bot.Run()
if err != nil {
panic(err)
}
} }

View file

@ -2,6 +2,7 @@ package tg
import ( import (
"errors" "errors"
"sort"
//"fmt" //"fmt"
@ -14,6 +15,8 @@ type User = tgbotapi.User
// The wrapper around Telegram API. // The wrapper around Telegram API.
type Bot struct { type Bot struct {
// Custom data value.
Data any
Api *tgbotapi.BotAPI Api *tgbotapi.BotAPI
Me *User Me *User
// Private bot behaviour. // Private bot behaviour.
@ -24,7 +27,6 @@ type Bot struct {
channelBehaviour *ChannelBehaviour channelBehaviour *ChannelBehaviour
sessions SessionMap sessions SessionMap
groupSessions GroupSessionMap groupSessions GroupSessionMap
value any
} }
@ -40,19 +42,6 @@ func NewBot(token string) (*Bot, error) {
}, nil }, nil
} }
// Set the custom global value for the bot,
// so it can be accessed from the callback
// functions.
func (bot *Bot) WithValue(v any) *Bot {
bot.value = v
return bot
}
// Get the global bot value.
func (bot *Bot) Value() any {
return bot.value
}
func (bot *Bot) Debug(debug bool) *Bot { func (bot *Bot) Debug(debug bool) *Bot {
bot.Api.Debug = debug bot.Api.Debug = debug
return bot return bot
@ -127,6 +116,40 @@ func (b *Bot) WithGroupSessions(sessions GroupSessionMap) *Bot {
return b return b
} }
// Setting the command on the user side.
func (bot *Bot) setCommands(
scope tgbotapi.BotCommandScope,
cmdMap map[CommandName] BotCommander,
) {
// First the private commands.
names := []string{}
for name := range cmdMap {
names = append(names, string(name))
}
sort.Strings([]string(names))
cmds := []BotCommander{}
for _, name := range names {
cmds = append(
cmds,
cmdMap[CommandName(name)],
)
}
botCmds := []tgbotapi.BotCommand{}
for _, cmd := range cmds {
botCmds = append(botCmds, cmd.ToApi())
}
//tgbotapi.NewBotCommandScopeAllPrivateChats(),
cfg := tgbotapi.NewSetMyCommandsWithScope(
scope,
botCmds...,
)
bot.Api.Request(cfg)
}
// Run the bot with the Behaviour. // Run the bot with the Behaviour.
func (bot *Bot) Run() error { func (bot *Bot) Run() error {
if bot.behaviour == nil && if bot.behaviour == nil &&
@ -139,12 +162,28 @@ func (bot *Bot) Run() error {
handles := make(map[string]chan *Update) handles := make(map[string]chan *Update)
if bot.behaviour != nil { if bot.behaviour != nil {
commanders := make(map[CommandName] BotCommander)
for k, v := range bot.behaviour.Commands {
commanders[k] = v
}
bot.setCommands(
tgbotapi.NewBotCommandScopeAllPrivateChats(),
commanders,
)
chn := make(chan *Update) chn := make(chan *Update)
handles["private"] = chn handles["private"] = chn
go bot.handlePrivate(chn) go bot.handlePrivate(chn)
} }
if bot.groupBehaviour != nil { if bot.groupBehaviour != nil {
commanders := make(map[CommandName] BotCommander)
for k, v := range bot.groupBehaviour.Commands {
commanders[k] = v
}
bot.setCommands(
tgbotapi.NewBotCommandScopeAllGroupChats(),
commanders,
)
chn := make(chan *Update) chn := make(chan *Update)
handles["group"] = chn handles["group"] = chn
handles["supergroup"] = chn handles["supergroup"] = chn

View file

@ -3,10 +3,13 @@ package tg
import ( import (
//"flag" //"flag"
apix "github.com/go-telegram-bot-api/telegram-bot-api/v5" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
type Message = apix.Message type BotCommander interface {
ToApi() tgbotapi.BotCommand
}
type Message = tgbotapi.Message
type CommandName string type CommandName string
type Command struct { type Command struct {
@ -31,6 +34,13 @@ func (c *Command) ActionFunc(af ActionFunc) *Command {
return c.WithAction(af) return c.WithAction(af)
} }
func (c *Command) ToApi() tgbotapi.BotCommand {
ret := tgbotapi.BotCommand{}
ret.Command = string(c.Name)
ret.Description = c.Description
return ret
}
func (c *Command) Desc(desc string) *Command { func (c *Command) Desc(desc string) *Command {
c.Description = desc c.Description = desc
return c return c
@ -62,3 +72,10 @@ func (cmd *GroupCommand) Desc(desc string) *GroupCommand {
cmd.Description = desc cmd.Description = desc
return cmd return cmd
} }
func (c *GroupCommand) ToApi() tgbotapi.BotCommand {
ret := tgbotapi.BotCommand{}
ret.Command = string(c.Name)
ret.Description = c.Description
return ret
}

View file

@ -3,7 +3,7 @@ package tg
import ( import (
"fmt" "fmt"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" //tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
// Customized actions for the group behaviour. // Customized actions for the group behaviour.
@ -56,7 +56,12 @@ func (c *groupContext) handleUpdateChan(updates chan *Update) {
cmdName := CommandName(msg.Command()) cmdName := CommandName(msg.Command())
// Skipping the commands sent not to us. // Skipping the commands sent not to us.
atName := msg.CommandWithAt()[len(cmdName)+1:] withAt := msg.CommandWithAt()
if len(cmdName) == len(withAt) {
continue
}
atName := withAt[len(cmdName)+1:]
if c.Bot.Me.UserName != atName { if c.Bot.Me.UserName != atName {
continue continue
} }
@ -74,16 +79,20 @@ func (c *groupContext) handleUpdateChan(updates chan *Update) {
} }
} }
func (c *groupContext) Sendf(format string, v ...any) error { func (c *groupContext) Sendf(
return c.Send(fmt.Sprintf(format, v...)) 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
} }
// Sends into the chat specified values converted to strings. // Sends into the chat specified values converted to strings.
func (c *groupContext) Send(v ...any) error { func (c *groupContext) Send(v Sendable) (*Message, error) {
msg := tgbotapi.NewMessage( return c.Bot.Send(c.Session.Id, v)
c.Session.Id.ToApi(),
fmt.Sprint(v...),
)
_, err := c.Bot.Api.Send(msg)
return err
} }

View file

@ -11,7 +11,7 @@ type context struct {
// To reach the bot abilities inside callbacks. // To reach the bot abilities inside callbacks.
Bot *Bot Bot *Bot
widgetUpdates chan *Update widgetUpdates chan *Update
CurScreen, PrevScreen *Screen curScreen, prevScreen *Screen
} }
// The type represents way to interact with user in // The type represents way to interact with user in
@ -71,10 +71,7 @@ func (c *context) handleUpdateChan(updates chan *Update) {
} }
func (c *context) run(a Action, u *Update) { func (c *context) run(a Action, u *Update) {
go a.Act(&Context{ a.Act(&Context{context: c, Update: u})
context: c,
Update: u,
})
} }
func (c *context) Render(v Renderable) ([]*Message, error) { func (c *context) Render(v Renderable) ([]*Message, error) {
@ -149,8 +146,8 @@ func (c *Context) ChangeScreen(screenId ScreenId) error {
// Getting the screen and changing to // Getting the screen and changing to
// then executing its widget. // then executing its widget.
screen := c.Bot.behaviour.Screens[screenId] screen := c.Bot.behaviour.Screens[screenId]
c.PrevScreen = c.CurScreen c.prevScreen = c.curScreen
c.CurScreen = screen c.curScreen = screen
// Making the new channel for the widget. // Making the new channel for the widget.
if c.widgetUpdates != nil { if c.widgetUpdates != nil {

View file

@ -6,10 +6,14 @@ type ScreenId string
// Screen statement of the bot. // Screen statement of the bot.
// Mostly what buttons to show. // Mostly what buttons to show.
type Screen struct { type Screen struct {
// Unique identifer to change to the screen
// via Context.ChangeScreen method.
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.
@ -23,3 +27,8 @@ func NewScreen(id ScreenId, widget Widget) *Screen {
} }
} }
func (s *Screen) WithDynamic(dynamic DynamicWidget) *Screen {
s.Dynamic = dynamic
return s
}

View file

@ -12,7 +12,20 @@ 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) Serve(*Context, chan *Update) error
}
// Implementing the interface provides
type DynamicWidget interface {
MakeWidget() Widget
}
// The function that implements the Widget
// interface.
type WidgetFunc func(*Context, chan *Update)
func (wf WidgetFunc) Serve(c *Context, updates chan *Update){
wf(c, updates)
} }
// The basic widget to provide keyboard functionality // The basic widget to provide keyboard functionality
@ -64,10 +77,10 @@ func (p *Page) WithSub(sub Widget) *Page {
func (p *Page) Serve( func (p *Page) Serve(
c *Context, updates chan *Update, c *Context, updates chan *Update,
) { ) error {
msgs, err := c.Render(p) msgs, err := c.Render(p)
if err != nil { if err != nil {
panic(err) return err
} }
// The inline message is always returned // The inline message is always returned
@ -128,7 +141,7 @@ func (p *Page) Serve(
_, err := c.Bot.Api.Request(cb) _, err := c.Bot.Api.Request(cb)
if err != nil { if err != nil {
panic(err) return err
} }
kbd := p.Inline kbd := p.Inline
if kbd == nil { if kbd == nil {
@ -156,6 +169,7 @@ func (p *Page) Serve(
c.run(act, u) c.run(act, u)
} }
} }
return nil
} }
func (s *Page) Render( func (s *Page) Render(