Skip to content

Commit 5acabf0

Browse files
committed
feat: SavePreparedInlineMessage
Stores an inline message a Mini App user can later send (messages.savePreparedInlineMessage), reusing the existing InlineQueryResult.toTg machinery. WithAllow{User,Bot,Group,Channel}Chats options map to MTProto inline query peer types. Returns PreparedInlineMessage{id, expiration_date}. Factors the repeated nil-result error into errNilInlineResult(). Hermetic test; lint clean.
1 parent 7c16fed commit 5acabf0

5 files changed

Lines changed: 180 additions & 2 deletions

File tree

answer.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ func (b *Bot) AnswerInlineQuery(
132132
tgResults := make([]tg.InputBotInlineResultClass, 0, len(results))
133133
for _, r := range results {
134134
if r == nil {
135-
return &Error{Code: 400, Description: "Bad Request: inline query result is nil"}
135+
return errNilInlineResult()
136136
}
137137

138138
converted, err := r.toTg(ctx, b)

answer_webapp.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type SentWebAppMessage struct {
2020
// originated. webAppQueryID is the query id from the Web App.
2121
func (b *Bot) AnswerWebAppQuery(ctx context.Context, webAppQueryID string, result InlineQueryResult) (*SentWebAppMessage, error) {
2222
if result == nil {
23-
return nil, &Error{Code: 400, Description: "Bad Request: inline query result is nil"}
23+
return nil, errNilInlineResult()
2424
}
2525

2626
converted, err := result.toTg(ctx, b)

inline_query_result.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ import (
99
// mimeImageJPEG is the default thumbnail MIME type for inline results.
1010
const mimeImageJPEG = "image/jpeg"
1111

12+
// errNilInlineResult is returned when a method is given a nil InlineQueryResult.
13+
func errNilInlineResult() *Error {
14+
return &Error{Code: 400, Description: "Bad Request: inline query result is nil"}
15+
}
16+
1217
// InlineQueryResult is a sealed union describing one result of an inline query.
1318
//
1419
// Concrete variants cover articles, fresh-by-URL media (photo/gif/mpeg4

prepared_inline.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/tg"
7+
)
8+
9+
// PreparedInlineMessage describes an inline message to be sent by a user of a Mini
10+
// App. It is the result of SavePreparedInlineMessage.
11+
type PreparedInlineMessage struct {
12+
// ID is the unique identifier of the prepared message.
13+
ID string `json:"id"`
14+
// ExpirationDate is the Unix time when the prepared message can no longer be
15+
// used.
16+
ExpirationDate int `json:"expiration_date"`
17+
}
18+
19+
// PreparedInlineMessageOption configures the set of chat types a prepared inline
20+
// message may be sent to.
21+
type PreparedInlineMessageOption func(*preparedInlineConfig)
22+
23+
type preparedInlineConfig struct {
24+
allowUsers bool
25+
allowBots bool
26+
allowGroups bool
27+
allowChannels bool
28+
}
29+
30+
// WithAllowUserChats permits the message to be sent to private chats with users.
31+
func WithAllowUserChats() PreparedInlineMessageOption {
32+
return func(c *preparedInlineConfig) { c.allowUsers = true }
33+
}
34+
35+
// WithAllowBotChats permits the message to be sent to private chats with bots.
36+
func WithAllowBotChats() PreparedInlineMessageOption {
37+
return func(c *preparedInlineConfig) { c.allowBots = true }
38+
}
39+
40+
// WithAllowGroupChats permits the message to be sent to group and supergroup chats.
41+
func WithAllowGroupChats() PreparedInlineMessageOption {
42+
return func(c *preparedInlineConfig) { c.allowGroups = true }
43+
}
44+
45+
// WithAllowChannelChats permits the message to be sent to channel chats.
46+
func WithAllowChannelChats() PreparedInlineMessageOption {
47+
return func(c *preparedInlineConfig) { c.allowChannels = true }
48+
}
49+
50+
// peerTypes turns the allow-* flags into MTProto inline query peer types.
51+
func (c preparedInlineConfig) peerTypes() []tg.InlineQueryPeerTypeClass {
52+
var out []tg.InlineQueryPeerTypeClass
53+
54+
if c.allowUsers {
55+
out = append(out, &tg.InlineQueryPeerTypePM{})
56+
}
57+
58+
if c.allowBots {
59+
out = append(out, &tg.InlineQueryPeerTypeBotPM{})
60+
}
61+
62+
if c.allowGroups {
63+
out = append(out, &tg.InlineQueryPeerTypeChat{}, &tg.InlineQueryPeerTypeMegagroup{})
64+
}
65+
66+
if c.allowChannels {
67+
out = append(out, &tg.InlineQueryPeerTypeBroadcast{})
68+
}
69+
70+
return out
71+
}
72+
73+
// SavePreparedInlineMessage stores a message that can later be sent by a user of a
74+
// Mini App. By default the message may not be sent anywhere; use the WithAllow*
75+
// options to permit specific chat types.
76+
func (b *Bot) SavePreparedInlineMessage(
77+
ctx context.Context, userID int64, result InlineQueryResult, opts ...PreparedInlineMessageOption,
78+
) (*PreparedInlineMessage, error) {
79+
if result == nil {
80+
return nil, errNilInlineResult()
81+
}
82+
83+
var cfg preparedInlineConfig
84+
85+
for _, o := range opts {
86+
o(&cfg)
87+
}
88+
89+
user, err := b.resolveInputUser(ctx, userID)
90+
if err != nil {
91+
return nil, err
92+
}
93+
94+
converted, err := result.toTg(ctx, b)
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
res, err := b.raw.MessagesSavePreparedInlineMessage(ctx, &tg.MessagesSavePreparedInlineMessageRequest{
100+
Result: converted,
101+
UserID: user,
102+
PeerTypes: cfg.peerTypes(),
103+
})
104+
if err != nil {
105+
return nil, asAPIError(err)
106+
}
107+
108+
return &PreparedInlineMessage{ID: res.ID, ExpirationDate: res.ExpireDate}, nil
109+
}

prepared_inline_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestSavePreparedInlineMessage(t *testing.T) {
11+
inv := newMockInvoker()
12+
inv.reply(tg.MessagesSavePreparedInlineMessageRequestTypeID, &tg.MessagesBotPreparedInlineMessage{
13+
ID: "prep1",
14+
ExpireDate: 1700000000,
15+
})
16+
17+
result := &InlineQueryResultArticle{
18+
ID: "r1",
19+
Title: "Title",
20+
InputMessageContent: &InputTextMessageContent{MessageText: "hi"},
21+
}
22+
23+
got, err := newMockBot(inv).SavePreparedInlineMessage(context.Background(), 99, result,
24+
WithAllowUserChats(), WithAllowChannelChats())
25+
if err != nil {
26+
t.Fatalf("SavePreparedInlineMessage: %v", err)
27+
}
28+
29+
if got.ID != "prep1" || got.ExpirationDate != 1700000000 {
30+
t.Fatalf("got = %#v", got)
31+
}
32+
33+
var req tg.MessagesSavePreparedInlineMessageRequest
34+
35+
inv.decode(t, tg.MessagesSavePreparedInlineMessageRequestTypeID, &req)
36+
37+
if _, ok := req.Result.(*tg.InputBotInlineResult); !ok {
38+
t.Fatalf("result = %#v, want inline result", req.Result)
39+
}
40+
41+
if len(req.PeerTypes) != 2 {
42+
t.Fatalf("peer types = %#v, want user + broadcast", req.PeerTypes)
43+
}
44+
45+
if _, ok := req.PeerTypes[0].(*tg.InlineQueryPeerTypePM); !ok {
46+
t.Fatalf("peer type 0 = %#v, want PM", req.PeerTypes[0])
47+
}
48+
49+
if _, ok := req.PeerTypes[1].(*tg.InlineQueryPeerTypeBroadcast); !ok {
50+
t.Fatalf("peer type 1 = %#v, want broadcast", req.PeerTypes[1])
51+
}
52+
}
53+
54+
func TestSavePreparedInlineMessageNilResult(t *testing.T) {
55+
inv := newMockInvoker()
56+
57+
if _, err := newMockBot(inv).SavePreparedInlineMessage(context.Background(), 99, nil); err == nil {
58+
t.Fatal("expected error for nil result")
59+
}
60+
61+
if inv.count() != 0 {
62+
t.Fatalf("made %d RPC calls, want 0", inv.count())
63+
}
64+
}

0 commit comments

Comments
 (0)