Interactive /mybots menu for @BotFather

Button-driven bot management: paginated picker, per-bot API token /
revoke, Edit Bot (name/description/about/commands/botpic), Bot Settings
toggles (inline/groups/privacy), and delete. Navigation edits the menu
message in place via a new editServiceBotMessage helper.

Edit Botpic accepts a photo the user sends to @BotFather and sets it as
the bot's profile photo (new files.SetAvatarFromExistingPhoto, wired
through bots.SetBotUserpic / WithBotAvatarStore); photos.uploadProfilePhoto
does not accept a bot target so this is the only route.
This commit is contained in:
Astra 2026-09-08 13:31:05 +01:00
parent 1ebf8ddb5a
commit 11142f74c5
10 changed files with 1467 additions and 26 deletions

View file

@ -31,10 +31,18 @@ func newOwner(t *testing.T, users *memory.UserStore, phone string) domain.User {
// sendToBotFather 同步驱动 responder(绕过 OnPrivateMessage 的 goroutine 派发以
// 保证单测确定性;异步派发由 mtprotoedge bot e2e 覆盖),返回 BotFather 最新回复文本。
func botFatherMsg(userID int64, text string) domain.Message {
return domain.Message{
From: domain.Peer{Type: domain.PeerTypeUser, ID: userID},
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID},
Body: text,
}
}
func sendToBotFather(t *testing.T, svc *Service, messages *memory.MessageStore, owner domain.User, text string) string {
t.Helper()
ctx := context.Background()
svc.respondAsBotFather(owner.ID, text)
svc.respondAsBotFather(owner.ID, botFatherMsg(owner.ID, text))
list, err := messages.ListByUser(ctx, owner.ID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID},
@ -332,8 +340,8 @@ func TestBotFatherMyBotsAndLimit(t *testing.T) {
t.Fatalf("create bot %d: %v", i, err)
}
}
if reply := sendToBotFather(t, svc, messages, owner, "/mybots"); !strings.Contains(reply, "@limit0_bot") {
t.Fatalf("/mybots reply = %q, want bot list", reply)
if reply := sendToBotFather(t, svc, messages, owner, "/mybots"); !strings.Contains(reply, "Choose a bot") {
t.Fatalf("/mybots reply = %q, want bot picker", reply)
}
if _, _, err := svc.CreateBot(ctx, owner.ID, "One Too Many", "toomany_bot"); err != domain.ErrBotsTooMany {
t.Fatalf("create over limit err = %v, want ErrBotsTooMany", err)
@ -384,7 +392,7 @@ func TestBotFatherReplyRespectsBlock(t *testing.T) {
owner := newOwner(t, users, "+1099")
ctx := context.Background()
svc.respondAsBotFather(owner.ID, "/help")
svc.respondAsBotFather(owner.ID, botFatherMsg(owner.ID, "/help"))
// IsBlocked 参数语义:owner(userID) 是否 block 了 BotFather(blockedUserID)。
if blocker.gotUser != owner.ID || blocker.gotPeer != domain.BotFatherUserID {
@ -408,7 +416,7 @@ func TestBotFatherReplyRespectsBlock(t *testing.T) {
// 未 block:回复正常投递。
blocker.blocked = false
other := newOwner(t, users, "+1098")
svc.respondAsBotFather(other.ID, "/help")
svc.respondAsBotFather(other.ID, botFatherMsg(other.ID, "/help"))
otherList, err := messages.ListByUser(ctx, other.ID, domain.MessageFilter{
HasPeer: true,
Peer: domain.Peer{Type: domain.PeerTypeUser, ID: domain.BotFatherUserID},