Skip to content

Commit 379a1b8

Browse files
ernadoclaude
andcommitted
feat: encode inline_message_id and populate it inbound
Adds the inline_message_id codec: base64url (unpadded) of the TL-serialized inputBotInlineMessageID, matching the official Bot API server (TDLib's base64url_encode(serialize(...))). decodeInlineMessageID is the inverse and tolerates padding. Wires the two inbound updates that carry an inline message id, which were previously undispatched: - updateBotInlineSend -> ChosenInlineResult (new OnChosenInlineResult handler), with InlineMessageID and Location populated - updateInlineBotCallbackQuery -> CallbackQuery with InlineMessageID (and no Message, since the callback is on an inline message) This populates the InlineMessageID fields that were declared but never set, and unblocks the outbound methods that consume an inline_message_id. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 7f267b6 commit 379a1b8

4 files changed

Lines changed: 248 additions & 7 deletions

File tree

inline_message_id.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package botapi
2+
3+
import (
4+
"encoding/base64"
5+
"strings"
6+
7+
"github.com/gotd/td/bin"
8+
"github.com/gotd/td/tg"
9+
)
10+
11+
// encodeInlineMessageID serializes an MTProto inline message id into the Bot API
12+
// inline_message_id string. It is the base64url (unpadded) TL serialization of
13+
// the inputBotInlineMessageID constructor, matching the official Bot API server
14+
// (which delegates to TDLib's base64url_encode(serialize(...))).
15+
func encodeInlineMessageID(id tg.InputBotInlineMessageIDClass) (string, error) {
16+
if id == nil {
17+
return "", nil
18+
}
19+
20+
var buf bin.Buffer
21+
22+
if err := id.Encode(&buf); err != nil {
23+
return "", &Error{Code: 500, Description: "Internal Server Error: " + err.Error()}
24+
}
25+
26+
return base64.RawURLEncoding.EncodeToString(buf.Buf), nil
27+
}
28+
29+
// decodeInlineMessageID parses a Bot API inline_message_id string back into the
30+
// MTProto inline message id. Padding, if present, is tolerated.
31+
func decodeInlineMessageID(s string) (tg.InputBotInlineMessageIDClass, error) {
32+
raw, err := base64.RawURLEncoding.DecodeString(strings.TrimRight(s, "="))
33+
if err != nil {
34+
return nil, errInvalidInlineMessageID()
35+
}
36+
37+
id, err := tg.DecodeInputBotInlineMessageID(&bin.Buffer{Buf: raw})
38+
if err != nil {
39+
return nil, errInvalidInlineMessageID()
40+
}
41+
42+
return id, nil
43+
}
44+
45+
// errInvalidInlineMessageID is returned for a malformed inline_message_id.
46+
func errInvalidInlineMessageID() error {
47+
return &Error{Code: 400, Description: "Bad Request: invalid inline_message_id"}
48+
}

inline_message_id_test.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package botapi
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gotd/td/tg"
7+
)
8+
9+
func TestInlineMessageIDRoundTrip(t *testing.T) {
10+
cases := []tg.InputBotInlineMessageIDClass{
11+
&tg.InputBotInlineMessageID{DCID: 2, ID: 123456789, AccessHash: -987654321},
12+
&tg.InputBotInlineMessageID64{DCID: 4, OwnerID: 42, ID: 7, AccessHash: 555},
13+
}
14+
15+
for _, want := range cases {
16+
enc, err := encodeInlineMessageID(want)
17+
if err != nil {
18+
t.Fatalf("encode %#v: %v", want, err)
19+
}
20+
21+
if enc == "" {
22+
t.Fatalf("empty encoding for %#v", want)
23+
}
24+
25+
got, err := decodeInlineMessageID(enc)
26+
if err != nil {
27+
t.Fatalf("decode %q: %v", enc, err)
28+
}
29+
30+
if got.String() != want.String() {
31+
t.Fatalf("round trip: got %#v, want %#v", got, want)
32+
}
33+
}
34+
}
35+
36+
func TestEncodeInlineMessageIDNil(t *testing.T) {
37+
enc, err := encodeInlineMessageID(nil)
38+
if err != nil {
39+
t.Fatalf("encode nil: %v", err)
40+
}
41+
42+
if enc != "" {
43+
t.Fatalf("nil encoding = %q, want empty", enc)
44+
}
45+
}
46+
47+
func TestDecodeInlineMessageIDInvalid(t *testing.T) {
48+
for _, s := range []string{"", "not base64 !!!", "AAAA"} {
49+
if _, err := decodeInlineMessageID(s); err == nil {
50+
t.Fatalf("expected error for %q", s)
51+
}
52+
}
53+
}
54+
55+
func TestChosenInlineResultFromTg(t *testing.T) {
56+
mid := &tg.InputBotInlineMessageID64{DCID: 2, OwnerID: 10, ID: 3, AccessHash: 99}
57+
58+
u := &tg.UpdateBotInlineSend{
59+
UserID: 10,
60+
Query: "q",
61+
ID: "result-1",
62+
}
63+
u.SetMsgID(mid)
64+
u.SetGeo(&tg.GeoPoint{Lat: 1.5, Long: 2.5, AccuracyRadius: 7})
65+
66+
e := tg.Entities{Users: map[int64]*tg.User{10: {ID: 10, FirstName: "Picker"}}}
67+
68+
got := chosenInlineResultFromTg(e, u)
69+
70+
if got.ResultID != "result-1" || got.Query != "q" || got.From.FirstName != "Picker" {
71+
t.Fatalf("chosen result = %#v", got)
72+
}
73+
74+
if got.Location == nil || got.Location.Latitude != 1.5 || got.Location.Longitude != 2.5 {
75+
t.Fatalf("location = %#v", got.Location)
76+
}
77+
78+
decoded, err := decodeInlineMessageID(got.InlineMessageID)
79+
if err != nil {
80+
t.Fatalf("decode inline message id: %v", err)
81+
}
82+
83+
if decoded.String() != mid.String() {
84+
t.Fatalf("inline message id = %#v, want %#v", decoded, mid)
85+
}
86+
}
87+
88+
func TestInlineCallbackQueryFromTg(t *testing.T) {
89+
mid := &tg.InputBotInlineMessageID{DCID: 1, ID: 5, AccessHash: 6}
90+
91+
u := &tg.UpdateInlineBotCallbackQuery{
92+
QueryID: 77,
93+
UserID: 10,
94+
MsgID: mid,
95+
ChatInstance: 88,
96+
Data: []byte("payload"),
97+
}
98+
99+
e := tg.Entities{Users: map[int64]*tg.User{10: {ID: 10, FirstName: "Tapper"}}}
100+
101+
got := inlineCallbackQueryFromTg(e, u)
102+
103+
if got.ID != "77" || got.Data != "payload" || got.From.FirstName != "Tapper" {
104+
t.Fatalf("callback = %#v", got)
105+
}
106+
107+
if got.Message != nil {
108+
t.Fatalf("inline callback should have no Message")
109+
}
110+
111+
decoded, err := decodeInlineMessageID(got.InlineMessageID)
112+
if err != nil {
113+
t.Fatalf("decode inline message id: %v", err)
114+
}
115+
116+
if decoded.String() != mid.String() {
117+
t.Fatalf("inline message id = %#v, want %#v", decoded, mid)
118+
}
119+
}

on.go

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ func (b *Bot) installHandlers() {
4141

4242
return nil
4343
})
44+
b.disp.OnBotInlineSend(func(ctx context.Context, e tg.Entities, u *tg.UpdateBotInlineSend) error {
45+
b.route(ctx, &Update{ChosenInlineResult: chosenInlineResultFromTg(e, u)})
46+
47+
return nil
48+
})
49+
b.disp.OnInlineBotCallbackQuery(func(ctx context.Context, e tg.Entities, u *tg.UpdateInlineBotCallbackQuery) error {
50+
b.route(ctx, &Update{CallbackQuery: inlineCallbackQueryFromTg(e, u)})
51+
52+
return nil
53+
})
4454
b.disp.OnBotShippingQuery(func(ctx context.Context, e tg.Entities, u *tg.UpdateBotShippingQuery) error {
4555
b.route(ctx, &Update{ShippingQuery: shippingQueryFromTg(e, u)})
4656

@@ -94,13 +104,14 @@ func (b *Bot) dispatchMessage(ctx context.Context, msg tg.MessageClass, edited b
94104

95105
// Kind predicates select an update by which field it carries. They are shared
96106
// by Bot.On* and Group.On*.
97-
func hasMessage(u *Update) bool { return u.Message != nil }
98-
func hasEditedMessage(u *Update) bool { return u.EditedMessage != nil }
99-
func hasChannelPost(u *Update) bool { return u.ChannelPost != nil }
100-
func hasCallbackQuery(u *Update) bool { return u.CallbackQuery != nil }
101-
func hasInlineQuery(u *Update) bool { return u.InlineQuery != nil }
102-
func hasShippingQuery(u *Update) bool { return u.ShippingQuery != nil }
103-
func hasPreCheckoutQuery(u *Update) bool { return u.PreCheckoutQuery != nil }
107+
func hasMessage(u *Update) bool { return u.Message != nil }
108+
func hasEditedMessage(u *Update) bool { return u.EditedMessage != nil }
109+
func hasChannelPost(u *Update) bool { return u.ChannelPost != nil }
110+
func hasCallbackQuery(u *Update) bool { return u.CallbackQuery != nil }
111+
func hasInlineQuery(u *Update) bool { return u.InlineQuery != nil }
112+
func hasChosenInlineResult(u *Update) bool { return u.ChosenInlineResult != nil }
113+
func hasShippingQuery(u *Update) bool { return u.ShippingQuery != nil }
114+
func hasPreCheckoutQuery(u *Update) bool { return u.PreCheckoutQuery != nil }
104115

105116
// OnMessage registers a handler for new messages matching the predicates.
106117
func (b *Bot) OnMessage(h Handler, predicates ...Predicate) {
@@ -127,6 +138,12 @@ func (b *Bot) OnInlineQuery(h Handler, predicates ...Predicate) {
127138
b.on(h, prepend(hasInlineQuery, predicates)...)
128139
}
129140

141+
// OnChosenInlineResult registers a handler for inline results chosen by users.
142+
// Requires inline feedback to be enabled with BotFather.
143+
func (b *Bot) OnChosenInlineResult(h Handler, predicates ...Predicate) {
144+
b.on(h, prepend(hasChosenInlineResult, predicates)...)
145+
}
146+
130147
// OnShippingQuery registers a handler for shipping queries (flexible invoices).
131148
func (b *Bot) OnShippingQuery(h Handler, predicates ...Predicate) {
132149
b.on(h, prepend(hasShippingQuery, predicates)...)

updates_map.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,63 @@ func callbackQueryFromTg(e tg.Entities, u *tg.UpdateBotCallbackQuery) *CallbackQ
3737
return cq
3838
}
3939

40+
// inlineCallbackQueryFromTg builds a CallbackQuery from a callback on an inline
41+
// message (one sent via an inline query result). Unlike a regular callback, it
42+
// carries an inline_message_id instead of a Message.
43+
func inlineCallbackQueryFromTg(e tg.Entities, u *tg.UpdateInlineBotCallbackQuery) *CallbackQuery {
44+
cq := &CallbackQuery{
45+
ID: strconv.FormatInt(u.QueryID, 10),
46+
ChatInstance: strconv.FormatInt(u.ChatInstance, 10),
47+
Data: string(u.Data),
48+
GameShortName: u.GameShortName,
49+
}
50+
51+
if id, err := encodeInlineMessageID(u.MsgID); err == nil {
52+
cq.InlineMessageID = id
53+
}
54+
55+
if user, ok := e.Users[u.UserID]; ok {
56+
cq.From = userFromTgUser(user)
57+
} else {
58+
cq.From = User{ID: u.UserID}
59+
}
60+
61+
return cq
62+
}
63+
64+
// chosenInlineResultFromTg builds a ChosenInlineResult from a bot inline-send
65+
// update (the user picked one of the bot's inline results).
66+
func chosenInlineResultFromTg(e tg.Entities, u *tg.UpdateBotInlineSend) *ChosenInlineResult {
67+
r := &ChosenInlineResult{
68+
ResultID: u.ID,
69+
Query: u.Query,
70+
}
71+
72+
if id, ok := u.GetMsgID(); ok {
73+
if enc, err := encodeInlineMessageID(id); err == nil {
74+
r.InlineMessageID = enc
75+
}
76+
}
77+
78+
if geo, ok := u.GetGeo(); ok {
79+
if g, ok := geo.(*tg.GeoPoint); ok {
80+
r.Location = &Location{
81+
Latitude: g.Lat,
82+
Longitude: g.Long,
83+
HorizontalAccuracy: float64(g.AccuracyRadius),
84+
}
85+
}
86+
}
87+
88+
if user, ok := e.Users[u.UserID]; ok {
89+
r.From = userFromTgUser(user)
90+
} else {
91+
r.From = User{ID: u.UserID}
92+
}
93+
94+
return r
95+
}
96+
4097
// inlineQueryFromTg builds an InlineQuery from a bot inline-query update.
4198
func inlineQueryFromTg(e tg.Entities, u *tg.UpdateBotInlineQuery) *InlineQuery {
4299
iq := &InlineQuery{

0 commit comments

Comments
 (0)