botfather: /start <bot> opens that bot's menu

The "Manage Bot" button on a bot's profile deep-links to @BotFather with
start=<bot username>. parseBotCommand dropped the argument, so /start <bot>
just replied with the generic greeting instead of the per-bot menu.

Route "/start <arg>" to the bot's "What do you want to do?" screen (same as
/mybots then tapping the bot) when <arg> names one of the sender's own bots by
username or id; empty or unknown args keep the greeting.
This commit is contained in:
Astra 2026-09-09 15:03:53 +01:00
parent 236e9ed25a
commit 86f9b61336
2 changed files with 89 additions and 0 deletions

View file

@ -274,6 +274,13 @@ func (s *Service) handleBotFather(ctx context.Context, userID int64, msg domain.
if cmd, ok := parseBotCommand(text); ok {
inValueStep := found && state.Step == botFatherStepValue
if !inValueStep || botFatherGlobalCommands[cmd] {
// "/start <bot>" (the "Manage Bot" deep link) jumps straight to that
// bot's menu, like /mybots then tapping the bot.
if cmd == "start" {
if arg := botCommandArg(text); arg != "" {
return s.handleBotFatherStart(ctx, userID, arg)
}
}
return s.handleBotFatherCommand(ctx, userID, cmd)
}
}
@ -368,6 +375,41 @@ func (s *Service) stepPrompt(state domain.BotChatState) botReply {
}
}
// handleBotFatherStart answers "/start <arg>". When <arg> names one of the
// user's own bots (by username or numeric id) it opens that bot's menu - the
// same "What do you want to do?" screen as /mybots then tapping the bot, which
// is what the "Manage Bot" button on a bot's profile links to. An empty or
// unknown arg falls back to the plain greeting.
func (s *Service) handleBotFatherStart(ctx context.Context, userID int64, arg string) botReply {
_ = s.bots.DeleteBotChatState(ctx, domain.BotFatherUserID, userID)
want := strings.ToLower(strings.TrimPrefix(strings.TrimSpace(arg), "@"))
if want == "" {
return botReply{Text: botFatherHelpText}
}
owned, err := s.ownedBots(ctx, userID)
if err != nil {
s.log.Error("botfather: list bots for start payload", zap.Int64("user_id", userID), zap.Error(err))
return internalReply()
}
for _, b := range owned {
if strings.EqualFold(b.user.Username, want) || strconv.FormatInt(b.user.ID, 10) == want {
state := domain.BotChatState{
BotUserID: domain.BotFatherUserID,
UserID: userID,
Command: mybotsCommand,
Step: mybotsStepMenu,
Draft: map[string]string{},
}
reply := s.myBotsBotMenu(&state, b)
if !s.saveMyBotsState(ctx, state) {
return internalReply()
}
return reply
}
}
return botReply{Text: botFatherHelpText}
}
func (s *Service) handleBotFatherCommand(ctx context.Context, userID int64, cmd string) botReply {
switch cmd {
case "start", "help":
@ -1157,3 +1199,16 @@ func parseBotCommand(text string) (string, bool) {
}
return strings.ToLower(cmd), true
}
// botCommandArg returns the trimmed argument after a leading "/cmd", e.g.
// "/start my_bot" -> "my_bot". Empty when there is no argument.
func botCommandArg(text string) string {
text = strings.TrimSpace(text)
if !strings.HasPrefix(text, "/") {
return ""
}
if i := strings.IndexAny(text, " \t\n"); i >= 0 {
return strings.TrimSpace(text[i+1:])
}
return ""
}