/mybots: Edit Bot summary screen, return-to-menu, clickable @mentions
- Edit Bot now shows the current value of every field (Name/About/ Description/Botpic/Commands) like BotFather, with real botpic status via a new PeerHasAvatar port method. - After editing a field the dialog lands back on a fresh Edit Bot menu (working "Back to bot" / "Bots list" buttons) instead of ending, so a follow-up button press no longer reports the button as expired. - Service-bot messages now render @username as a tappable mention entity.
This commit is contained in:
parent
f474a360d7
commit
5afafc60c4
7 changed files with 215 additions and 10 deletions
|
|
@ -36,6 +36,10 @@ const (
|
|||
mybotsDraftGeneration = "gen"
|
||||
mybotsDraftPage = "page"
|
||||
mybotsDraftOptionPrefix = "opt:"
|
||||
// mybotsDraftReturn marks a botFatherStepValue state that was opened from the
|
||||
// /mybots menu, so a successful field edit lands back on the Edit Bot menu
|
||||
// (a fresh message with working buttons) instead of just ending the dialog.
|
||||
mybotsDraftReturn = "mb_ret"
|
||||
|
||||
// mybotsCallbackDataPrefix tags this menu's callback data. It carries no
|
||||
// information beyond "this is a @BotFather /mybots button".
|
||||
|
|
@ -211,7 +215,7 @@ func (s *Service) applyMyBotsChoice(ctx context.Context, state domain.BotChatSta
|
|||
switch {
|
||||
case errors.Is(err, domain.ErrBotSessionsNotRevoked):
|
||||
return s.myBotsTokenScreen(ctx, &state, b,
|
||||
fmt.Sprintf("Token for @%s changed, but I couldn't cut off sessions that are already logged in — tap Revoke again to be sure.", b.user.Username))
|
||||
fmt.Sprintf("Token for @%s changed, but I couldn't cut off sessions that are already logged in - tap Revoke again to be sure.", b.user.Username))
|
||||
case err != nil:
|
||||
s.log.Error("botfather: revoke token", zap.Int64("bot_user_id", b.user.ID), zap.Error(err))
|
||||
return internalReply()
|
||||
|
|
@ -222,7 +226,7 @@ func (s *Service) applyMyBotsChoice(ctx context.Context, state domain.BotChatSta
|
|||
|
||||
case strings.HasPrefix(choice, mybotsChoiceEditPrefix):
|
||||
return s.mybotsWithBot(ctx, &state, choice, mybotsChoiceEditPrefix, edit, func(b ownedBot) botReply {
|
||||
return s.myBotsEditMenu(&state, b)
|
||||
return s.myBotsEditMenu(ctx, &state, b)
|
||||
})
|
||||
|
||||
case strings.HasPrefix(choice, mybotsChoiceCfgPrefix) && !strings.HasPrefix(choice, mybotsChoiceCfgInline) &&
|
||||
|
|
@ -376,6 +380,8 @@ func (s *Service) mybotsBeginValueInput(ctx context.Context, state domain.BotCha
|
|||
Draft: map[string]string{
|
||||
botFatherDraftBotID: strconv.FormatInt(b.user.ID, 10),
|
||||
botFatherDraftBotUsername: b.user.Username,
|
||||
mybotsDraftReturn: "1",
|
||||
mybotsDraftPage: state.Draft[mybotsDraftPage],
|
||||
},
|
||||
}
|
||||
if err := s.bots.UpsertBotChatState(ctx, next); err != nil {
|
||||
|
|
@ -482,7 +488,7 @@ func (s *Service) myBotsTokenScreenWithToken(state *domain.BotChatState, b owned
|
|||
head += "\n\n"
|
||||
}
|
||||
head += fmt.Sprintf("Token for @%s:\n", b.user.Username)
|
||||
reply := tokenReply(head, token, "\n\nKeep it secret — anyone with this token controls the bot.")
|
||||
reply := tokenReply(head, token, "\n\nKeep it secret - anyone with this token controls the bot.")
|
||||
rows := [][]mybotsOption{
|
||||
{{text: "Revoke current token", choice: mybotsChoiceRevokePrefix + botID64(b), style: domain.MarkupButtonStyleDanger}},
|
||||
{{text: "‹ Back", choice: mybotsChoiceBotPrefix + botID64(b)}},
|
||||
|
|
@ -502,21 +508,109 @@ func (s *Service) myBotsRevokeConfirm(state *domain.BotChatState, b ownedBot) bo
|
|||
}
|
||||
}
|
||||
|
||||
func (s *Service) myBotsEditMenu(state *domain.BotChatState, b ownedBot) botReply {
|
||||
func (s *Service) myBotsEditMenu(ctx context.Context, state *domain.BotChatState, b ownedBot) botReply {
|
||||
name, about, description, err := s.GetBotInfo(ctx, b.user.ID)
|
||||
if err != nil {
|
||||
s.log.Error("botfather: get bot info for edit menu", zap.Int64("bot_user_id", b.user.ID), zap.Error(err))
|
||||
return internalReply()
|
||||
}
|
||||
commands, err := s.GetBotCommands(ctx, b.user.ID)
|
||||
if err != nil {
|
||||
s.log.Error("botfather: get bot commands for edit menu", zap.Int64("bot_user_id", b.user.ID), zap.Error(err))
|
||||
return internalReply()
|
||||
}
|
||||
page := mybotsDraftInt(*state, mybotsDraftPage)
|
||||
rows := [][]mybotsOption{
|
||||
{{text: "Edit Name", choice: mybotsChoiceSetNamePrefix + botID64(b)}},
|
||||
{{text: "Edit Description", choice: mybotsChoiceSetDescPrefix + botID64(b)}},
|
||||
{{text: "Edit About", choice: mybotsChoiceSetAboutPrefix + botID64(b)}},
|
||||
{{text: "Edit Description", choice: mybotsChoiceSetDescPrefix + botID64(b)}},
|
||||
{{text: "Edit Botpic", choice: mybotsChoiceBotpicPrefix + botID64(b)}},
|
||||
{{text: "Edit Commands", choice: mybotsChoiceSetCmdsPrefix + botID64(b)}},
|
||||
{{text: "‹ Back", choice: mybotsChoiceBotPrefix + botID64(b)}},
|
||||
{
|
||||
{text: "‹ Back to bot", choice: mybotsChoiceBotPrefix + botID64(b)},
|
||||
{text: "‹‹ Bots list", choice: mybotsChoiceListPrefix + strconv.FormatInt(page, 10)},
|
||||
},
|
||||
}
|
||||
return botReply{
|
||||
Text: fmt.Sprintf("Editing @%s. Pick a field — I'll ask for the new value.", b.user.Username),
|
||||
Text: myBotsEditSummary(b.user.Username, name, about, description, commands, s.botHasAvatar(ctx, b.user.ID)),
|
||||
ReplyMarkup: s.mybotsKeyboard(state, rows),
|
||||
}
|
||||
}
|
||||
|
||||
// myBotsEditSummary renders the "Edit @bot info" screen: the current value of
|
||||
// every editable field, mirroring what BotFather shows.
|
||||
func myBotsEditSummary(username, name, about, description string, commands []domain.BotCommand, hasBotpic bool) string {
|
||||
orNone := func(v string) string {
|
||||
v = strings.ReplaceAll(strings.TrimSpace(v), "\n", " ")
|
||||
if v == "" {
|
||||
return "🚫"
|
||||
}
|
||||
if r := []rune(v); len(r) > 120 {
|
||||
v = string(r[:117]) + "..."
|
||||
}
|
||||
return v
|
||||
}
|
||||
cmds := "no commands yet"
|
||||
if n := len(commands); n == 1 {
|
||||
cmds = "1 command"
|
||||
} else if n > 1 {
|
||||
cmds = fmt.Sprintf("%d commands", n)
|
||||
}
|
||||
botpic := "🚫 no botpic"
|
||||
if hasBotpic {
|
||||
botpic = "🖼 has a botpic"
|
||||
}
|
||||
return fmt.Sprintf(
|
||||
"Edit @%s info.\n\nName: %s\nAbout: %s\nDescription: %s\nDescription picture: 🚫 no description picture\nBotpic: %s\nCommands: %s\nPrivacy Policy: 🚫",
|
||||
username, orNone(name), orNone(about), orNone(description), botpic, cmds,
|
||||
)
|
||||
}
|
||||
|
||||
// botHasAvatar reports whether the bot currently has a profile photo. Without a
|
||||
// file layer wired it answers false.
|
||||
func (s *Service) botHasAvatar(ctx context.Context, botUserID int64) bool {
|
||||
if s.botAvatar == nil {
|
||||
return false
|
||||
}
|
||||
ok, err := s.botAvatar.PeerHasAvatar(ctx, domain.PeerTypeUser, botUserID)
|
||||
if err != nil {
|
||||
s.log.Warn("botfather: check bot avatar", zap.Int64("bot_user_id", botUserID), zap.Error(err))
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
// myBotsReturnToEditMenu rebuilds a fresh /mybots dialog on the Edit Bot menu
|
||||
// after a field was edited through the shared value-input flow, so the follow-up
|
||||
// message carries working "Back to bot" / "Bots list" buttons instead of the
|
||||
// dialog just ending.
|
||||
func (s *Service) myBotsReturnToEditMenu(ctx context.Context, userID, botID int64, page, lead string) botReply {
|
||||
b, ok, err := s.myBotForUser(ctx, userID, botID)
|
||||
if err != nil || !ok {
|
||||
s.clearState(ctx, userID)
|
||||
return botReply{Text: strings.TrimSpace(lead)}
|
||||
}
|
||||
st := domain.BotChatState{
|
||||
BotUserID: domain.BotFatherUserID,
|
||||
UserID: userID,
|
||||
Command: mybotsCommand,
|
||||
Step: mybotsStepMenu,
|
||||
Draft: map[string]string{},
|
||||
}
|
||||
if p := strings.TrimSpace(page); p != "" {
|
||||
st.Draft[mybotsDraftPage] = p
|
||||
}
|
||||
menu := s.myBotsEditMenu(ctx, &st, b)
|
||||
if menu.ReplyMarkup == nil || !s.saveMyBotsState(ctx, st) {
|
||||
s.clearState(ctx, userID)
|
||||
return botReply{Text: strings.TrimSpace(lead)}
|
||||
}
|
||||
if lead = strings.TrimSpace(lead); lead != "" {
|
||||
menu.Text = lead + "\n\n" + menu.Text
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
func (s *Service) myBotsSettingsScreen(state *domain.BotChatState, b ownedBot, lead string) botReply {
|
||||
inlineOn := b.profile.InlinePlaceholder != ""
|
||||
groupsOn := !b.profile.Nochats
|
||||
|
|
@ -529,9 +623,9 @@ func (s *Service) myBotsSettingsScreen(state *domain.BotChatState, b ownedBot, l
|
|||
{{text: "‹ Back", choice: mybotsChoiceBotPrefix + botID64(b)}},
|
||||
}
|
||||
body := fmt.Sprintf("Settings for @%s. Tap a row to flip it.\n\n"+
|
||||
"• Inline Mode — %s\n"+
|
||||
"• Allow Groups — %s (can the bot be added to groups)\n"+
|
||||
"• Group Privacy — %s (on = only sees commands and replies in groups)",
|
||||
"- Inline Mode: %s\n"+
|
||||
"- Allow Groups: %s (can the bot be added to groups)\n"+
|
||||
"- Group Privacy: %s (on = only sees commands and replies in groups)",
|
||||
b.user.Username, onOff(inlineOn), onOff(groupsOn), onOff(privacyOn))
|
||||
if lead != "" {
|
||||
body = lead + "\n\n" + body
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue