Skip to content

Commit 1344f22

Browse files
committed
feat: SavePreparedKeyboardButton + request-peer keyboard buttons
Adds request_users / request_chat support to KeyboardButton (KeyboardButtonRequestUsers / KeyboardButtonRequestChat), converted to MTProto keyboardButtonRequestPeer with RequestPeerTypeUser/Chat/Broadcast (reusing ChatAdminRights.toTg). SavePreparedKeyboardButton stores such a button via bots.requestWebViewButton and returns its id (PreparedKeyboardButton). Display- only fields (request_name/username/photo/title) are kept on the type but not carried over MTProto. Mapping confirmed against telegram-bot-api 10.1. Hermetic tests; lint clean.
1 parent e39a5cb commit 1344f22

5 files changed

Lines changed: 253 additions & 5 deletions

File tree

markup.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@ type KeyboardButtonPollType struct {
4040
// KeyboardButton is one button of a reply (custom) keyboard. Text is sent as a
4141
// plain message when no request/web-app field is set.
4242
type KeyboardButton struct {
43-
Text string `json:"text"`
44-
RequestContact bool `json:"request_contact,omitempty"`
45-
RequestLocation bool `json:"request_location,omitempty"`
46-
RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"`
47-
WebApp *WebAppInfo `json:"web_app,omitempty"`
43+
Text string `json:"text"`
44+
RequestUsers *KeyboardButtonRequestUsers `json:"request_users,omitempty"`
45+
RequestChat *KeyboardButtonRequestChat `json:"request_chat,omitempty"`
46+
RequestContact bool `json:"request_contact,omitempty"`
47+
RequestLocation bool `json:"request_location,omitempty"`
48+
RequestPoll *KeyboardButtonPollType `json:"request_poll,omitempty"`
49+
WebApp *WebAppInfo `json:"web_app,omitempty"`
4850
}
4951

5052
// ReplyKeyboardMarkup is a custom keyboard with reply options.

markup_to_tg.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ func inlineButtonToTg(btn InlineKeyboardButton) (tg.KeyboardButtonClass, error)
9191

9292
func keyboardButtonToTg(btn KeyboardButton) tg.KeyboardButtonClass {
9393
switch {
94+
case btn.RequestUsers != nil:
95+
return requestUsersToTg(btn.Text, btn.RequestUsers)
96+
case btn.RequestChat != nil:
97+
return requestChatToTg(btn.Text, btn.RequestChat)
9498
case btn.RequestContact:
9599
return markup.RequestPhone(btn.Text)
96100
case btn.RequestLocation:

prepared_keyboard.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/tg"
7+
)
8+
9+
// PreparedKeyboardButton is the result of SavePreparedKeyboardButton: a button a
10+
// user of a Mini App can later add to a reply keyboard.
11+
type PreparedKeyboardButton struct {
12+
// ID is the unique identifier of the prepared button.
13+
ID string `json:"id"`
14+
}
15+
16+
// SavePreparedKeyboardButton stores a keyboard button that a user of a Mini App
17+
// can later add to a reply keyboard. The button must request users or a chat
18+
// (i.e. have RequestUsers or RequestChat set).
19+
func (b *Bot) SavePreparedKeyboardButton(ctx context.Context, userID int64, button KeyboardButton) (*PreparedKeyboardButton, error) {
20+
if button.RequestUsers == nil && button.RequestChat == nil {
21+
return nil, &Error{Code: 400, Description: "Bad Request: button must request users or a chat"}
22+
}
23+
24+
user, err := b.resolveInputUser(ctx, userID)
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
res, err := b.raw.BotsRequestWebViewButton(ctx, &tg.BotsRequestWebViewButtonRequest{
30+
UserID: user,
31+
Button: keyboardButtonToTg(button),
32+
})
33+
if err != nil {
34+
return nil, asAPIError(err)
35+
}
36+
37+
return &PreparedKeyboardButton{ID: res.WebappReqID}, nil
38+
}

prepared_keyboard_test.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestSavePreparedKeyboardButtonUsers(t *testing.T) {
11+
inv := newMockInvoker()
12+
inv.reply(tg.BotsRequestWebViewButtonRequestTypeID, &tg.BotsRequestedButton{WebappReqID: "btn1"})
13+
14+
isBot := false
15+
button := KeyboardButton{
16+
Text: "Pick a user",
17+
RequestUsers: &KeyboardButtonRequestUsers{RequestID: 7, UserIsBot: &isBot, MaxQuantity: 3},
18+
}
19+
20+
got, err := newMockBot(inv).SavePreparedKeyboardButton(context.Background(), 99, button)
21+
if err != nil {
22+
t.Fatalf("SavePreparedKeyboardButton: %v", err)
23+
}
24+
25+
if got.ID != "btn1" {
26+
t.Fatalf("id = %q", got.ID)
27+
}
28+
29+
var req tg.BotsRequestWebViewButtonRequest
30+
31+
inv.decode(t, tg.BotsRequestWebViewButtonRequestTypeID, &req)
32+
33+
rp, ok := req.Button.(*tg.KeyboardButtonRequestPeer)
34+
if !ok || rp.ButtonID != 7 || rp.MaxQuantity != 3 {
35+
t.Fatalf("button = %#v", req.Button)
36+
}
37+
38+
ut, ok := rp.PeerType.(*tg.RequestPeerTypeUser)
39+
if !ok {
40+
t.Fatalf("peer type = %#v, want user", rp.PeerType)
41+
}
42+
43+
if bot, ok := ut.GetBot(); !ok || bot {
44+
t.Fatalf("bot = %v ok=%v, want false", bot, ok)
45+
}
46+
}
47+
48+
func TestSavePreparedKeyboardButtonChat(t *testing.T) {
49+
inv := newMockInvoker()
50+
inv.reply(tg.BotsRequestWebViewButtonRequestTypeID, &tg.BotsRequestedButton{WebappReqID: "btn2"})
51+
52+
button := KeyboardButton{
53+
Text: "Pick a channel",
54+
RequestChat: &KeyboardButtonRequestChat{RequestID: 9, ChatIsChannel: true, ChatIsCreated: true},
55+
}
56+
57+
if _, err := newMockBot(inv).SavePreparedKeyboardButton(context.Background(), 99, button); err != nil {
58+
t.Fatalf("SavePreparedKeyboardButton: %v", err)
59+
}
60+
61+
var req tg.BotsRequestWebViewButtonRequest
62+
63+
inv.decode(t, tg.BotsRequestWebViewButtonRequestTypeID, &req)
64+
65+
rp := req.Button.(*tg.KeyboardButtonRequestPeer)
66+
67+
bc, ok := rp.PeerType.(*tg.RequestPeerTypeBroadcast)
68+
if !ok || !bc.Creator {
69+
t.Fatalf("peer type = %#v, want broadcast creator", rp.PeerType)
70+
}
71+
}
72+
73+
func TestSavePreparedKeyboardButtonInvalid(t *testing.T) {
74+
inv := newMockInvoker()
75+
76+
if _, err := newMockBot(inv).SavePreparedKeyboardButton(context.Background(), 99, KeyboardButton{Text: "x"}); err == nil {
77+
t.Fatal("expected error for non-request button")
78+
}
79+
80+
if inv.count() != 0 {
81+
t.Fatalf("made %d RPC calls, want 0", inv.count())
82+
}
83+
}

request_peer_button.go

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package botapi
2+
3+
import "github.com/gotd/td/tg"
4+
5+
// KeyboardButtonRequestUsers defines the criteria used to request suitable users
6+
// when pressed. Maps to a request-peer keyboard button.
7+
type KeyboardButtonRequestUsers struct {
8+
// RequestID is an identifier echoed back in the users_shared service message.
9+
RequestID int
10+
// UserIsBot, when set, requires the selected users to be bots (true) or
11+
// regular users (false).
12+
UserIsBot *bool
13+
// UserIsPremium, when set, requires the selected users to have Telegram
14+
// Premium (true) or not (false).
15+
UserIsPremium *bool
16+
// MaxQuantity is the maximum number of users to select (1-10, default 1).
17+
MaxQuantity int
18+
// RequestName, RequestUsername and RequestPhoto request that the selected
19+
// users' details be included client-side. They are not enforced over MTProto.
20+
RequestName bool
21+
RequestUsername bool
22+
RequestPhoto bool
23+
}
24+
25+
// KeyboardButtonRequestChat defines the criteria used to request a suitable chat
26+
// when pressed. Maps to a request-peer keyboard button.
27+
type KeyboardButtonRequestChat struct {
28+
// RequestID is an identifier echoed back in the chat_shared service message.
29+
RequestID int
30+
// ChatIsChannel requires a channel (true) or a group/supergroup (false).
31+
ChatIsChannel bool
32+
// ChatIsForum, when set, requires a forum (true) or non-forum (false) chat.
33+
ChatIsForum *bool
34+
// ChatHasUsername, when set, requires a chat with (true) or without (false) a
35+
// public username.
36+
ChatHasUsername *bool
37+
// ChatIsCreated requires a chat owned by the user.
38+
ChatIsCreated bool
39+
// UserAdministratorRights requires the user to hold these rights in the chat.
40+
UserAdministratorRights *ChatAdminRights
41+
// BotAdministratorRights requires the bot to hold these rights in the chat.
42+
BotAdministratorRights *ChatAdminRights
43+
// BotIsMember requires the bot to be a member of the chat.
44+
BotIsMember bool
45+
// RequestTitle, RequestUsername and RequestPhoto request that the chat's
46+
// details be included client-side. They are not enforced over MTProto.
47+
RequestTitle bool
48+
RequestUsername bool
49+
RequestPhoto bool
50+
}
51+
52+
// requestUsersToTg builds the MTProto request-peer button for a user request.
53+
func requestUsersToTg(text string, r *KeyboardButtonRequestUsers) *tg.KeyboardButtonRequestPeer {
54+
peerType := &tg.RequestPeerTypeUser{}
55+
if r.UserIsBot != nil {
56+
peerType.SetBot(*r.UserIsBot)
57+
}
58+
59+
if r.UserIsPremium != nil {
60+
peerType.SetPremium(*r.UserIsPremium)
61+
}
62+
63+
maxQuantity := r.MaxQuantity
64+
if maxQuantity == 0 {
65+
maxQuantity = 1
66+
}
67+
68+
return &tg.KeyboardButtonRequestPeer{
69+
Text: text,
70+
ButtonID: r.RequestID,
71+
PeerType: peerType,
72+
MaxQuantity: maxQuantity,
73+
}
74+
}
75+
76+
// requestChatToTg builds the MTProto request-peer button for a chat request.
77+
func requestChatToTg(text string, r *KeyboardButtonRequestChat) *tg.KeyboardButtonRequestPeer {
78+
var peerType tg.RequestPeerTypeClass
79+
80+
if r.ChatIsChannel {
81+
broadcast := &tg.RequestPeerTypeBroadcast{Creator: r.ChatIsCreated}
82+
if r.UserAdministratorRights != nil {
83+
broadcast.SetUserAdminRights(r.UserAdministratorRights.toTg())
84+
}
85+
86+
if r.BotAdministratorRights != nil {
87+
broadcast.SetBotAdminRights(r.BotAdministratorRights.toTg())
88+
}
89+
90+
if r.ChatHasUsername != nil {
91+
broadcast.SetHasUsername(*r.ChatHasUsername)
92+
}
93+
94+
peerType = broadcast
95+
} else {
96+
chat := &tg.RequestPeerTypeChat{Creator: r.ChatIsCreated, BotParticipant: r.BotIsMember}
97+
if r.UserAdministratorRights != nil {
98+
chat.SetUserAdminRights(r.UserAdministratorRights.toTg())
99+
}
100+
101+
if r.BotAdministratorRights != nil {
102+
chat.SetBotAdminRights(r.BotAdministratorRights.toTg())
103+
}
104+
105+
if r.ChatHasUsername != nil {
106+
chat.SetHasUsername(*r.ChatHasUsername)
107+
}
108+
109+
if r.ChatIsForum != nil {
110+
chat.SetForum(*r.ChatIsForum)
111+
}
112+
113+
peerType = chat
114+
}
115+
116+
return &tg.KeyboardButtonRequestPeer{
117+
Text: text,
118+
ButtonID: r.RequestID,
119+
PeerType: peerType,
120+
}
121+
}

0 commit comments

Comments
 (0)