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
}
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 {
if u.Message == nil {
continue
@ -34,6 +34,7 @@ func (w *MutateMessageWidget) Serve(c *tg.Context, updates chan *tg.Update) {
text := u.Message.Text
c.Sendf("%s", w.Mutate(text))
}
return nil
}
func ExtractSessionData(c *tg.Context) *SessionData {
@ -173,47 +174,47 @@ var beh = tg.NewBehaviour().
),
),
),
).WithCommands(
tg.NewCommand("start").
Desc("start or restart the bot or move to the start screen").
ActionFunc(func(c *tg.Context){
c.ChangeScreen("start")
}),
tg.NewCommand("hello").
Desc("sends the 'Hello, World!' message back").
ActionFunc(func(c *tg.Context) {
c.Sendf("Hello, World!")
}),
tg.NewCommand("read").
Desc("reads a string and sends it back").
ActionFunc(func(c *tg.Context) {
/*c.Sendf("Type some text:")
msg, err := c.ReadTextMessage()
if err != nil {
return
}
c.Sendf("You typed %q", msg)*/
}),
tg.NewCommand("image").
Desc("sends a sample image").
ActionFunc(func(c *tg.Context) {
img := tg.NewFile("media/cat.jpg").Image().Caption("A cat!")
c.Send(img)
}),
tg.NewCommand("botname").
Desc("get the bot name").
ActionFunc(func(c *tg.Context) {
bd := c.Bot.Value().(*BotData)
c.Sendf("My name is %q", bd.Name)
}),
)
).WithCommands(
tg.NewCommand("start").
Desc("start or restart the bot or move to the start screen").
ActionFunc(func(c *tg.Context){
c.ChangeScreen("start")
}),
tg.NewCommand("hello").
Desc("sends the 'Hello, World!' message back").
ActionFunc(func(c *tg.Context) {
c.Sendf("Hello, World!")
}),
tg.NewCommand("read").
Desc("reads a string and sends it back").
ActionFunc(func(c *tg.Context) {
/*c.Sendf("Type some text:")
msg, err := c.ReadTextMessage()
if err != nil {
return
}
c.Sendf("You typed %q", msg)*/
}),
tg.NewCommand("image").
Desc("sends a sample image").
ActionFunc(func(c *tg.Context) {
img := tg.NewFile("media/cat.jpg").Image().Caption("A cat!")
c.Send(img)
}),
tg.NewCommand("botname").
Desc("get the bot name").
ActionFunc(func(c *tg.Context) {
bd := c.Bot.Data.(*BotData)
c.Sendf("My name is %q", bd.Name)
}),
)
var gBeh = tg.NewGroupBehaviour().
InitFunc(func(c *tg.GC) {
}).
WithCommands(
tg.NewGroupCommand("hello").ActionFunc(func(c *tg.GC) {
c.Send("Hello, World!")
c.Sendf("Hello, World!")
}),
tg.NewGroupCommand("mycounter").ActionFunc(func(c *tg.GC) {
d := c.Session().Data.(*SessionData)
@ -231,11 +232,15 @@ func main() {
bot = bot.
WithBehaviour(beh).
WithGroupBehaviour(gBeh).
WithValue(&BotData{
Name: "Jay",
}).
Debug(true)
bot.Data = &BotData{
Name: "Jay",
}
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 (
"errors"
"sort"
//"fmt"
@ -14,6 +15,8 @@ type User = tgbotapi.User
// The wrapper around Telegram API.
type Bot struct {
// Custom data value.
Data any
Api *tgbotapi.BotAPI
Me *User
// Private bot behaviour.
@ -24,7 +27,6 @@ type Bot struct {
channelBehaviour *ChannelBehaviour
sessions SessionMap
groupSessions GroupSessionMap
value any
}
@ -40,19 +42,6 @@ func NewBot(token string) (*Bot, error) {
}, 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 {
bot.Api.Debug = debug
return bot
@ -127,6 +116,40 @@ func (b *Bot) WithGroupSessions(sessions GroupSessionMap) *Bot {
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.
func (bot *Bot) Run() error {
if bot.behaviour == nil &&
@ -139,12 +162,28 @@ func (bot *Bot) Run() error {
handles := make(map[string]chan *Update)
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)
handles["private"] = chn
go bot.handlePrivate(chn)
}
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)
handles["group"] = chn
handles["supergroup"] = chn

View file

@ -3,10 +3,13 @@ package tg
import (
//"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 Command struct {
@ -31,6 +34,13 @@ func (c *Command) ActionFunc(af ActionFunc) *Command {
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 {
c.Description = desc
return c
@ -62,3 +72,10 @@ func (cmd *GroupCommand) Desc(desc string) *GroupCommand {
cmd.Description = desc
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 (
"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.
@ -56,7 +56,12 @@ func (c *groupContext) handleUpdateChan(updates chan *Update) {
cmdName := CommandName(msg.Command())
// 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 {
continue
}
@ -74,16 +79,20 @@ func (c *groupContext) handleUpdateChan(updates chan *Update) {
}
}
func (c *groupContext) Sendf(format string, v ...any) error {
return c.Send(fmt.Sprintf(format, v...))
func (c *groupContext) 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
}
// 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
func (c *groupContext) Send(v Sendable) (*Message, error) {
return c.Bot.Send(c.Session.Id, v)
}

View file

@ -11,7 +11,7 @@ type context struct {
// To reach the bot abilities inside callbacks.
Bot *Bot
widgetUpdates chan *Update
CurScreen, PrevScreen *Screen
curScreen, prevScreen *Screen
}
// 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) {
go a.Act(&Context{
context: c,
Update: u,
})
a.Act(&Context{context: c, Update: u})
}
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
// then executing its widget.
screen := c.Bot.behaviour.Screens[screenId]
c.PrevScreen = c.CurScreen
c.CurScreen = screen
c.prevScreen = c.curScreen
c.curScreen = screen
// Making the new channel for the widget.
if c.widgetUpdates != nil {

View file

@ -6,10 +6,14 @@ type ScreenId string
// Screen statement of the bot.
// Mostly what buttons to show.
type Screen struct {
// Unique identifer to change to the screen
// via Context.ChangeScreen method.
Id ScreenId
// The widget to run when reaching the screen.
Widget Widget
// Needs implementation later.
Dynamic DynamicWidget
}
// 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.
// Mostly made by looping over the
// 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
@ -64,10 +77,10 @@ func (p *Page) WithSub(sub Widget) *Page {
func (p *Page) Serve(
c *Context, updates chan *Update,
) {
) error {
msgs, err := c.Render(p)
if err != nil {
panic(err)
return err
}
// The inline message is always returned
@ -128,7 +141,7 @@ func (p *Page) Serve(
_, err := c.Bot.Api.Request(cb)
if err != nil {
panic(err)
return err
}
kbd := p.Inline
if kbd == nil {
@ -156,6 +169,7 @@ func (p *Page) Serve(
c.run(act, u)
}
}
return nil
}
func (s *Page) Render(