Skip to content

Commit 1ecd431

Browse files
committed
feat: SendRichMessageDraft via native page blocks
The HTTP Bot API only takes a rich message as an HTML/Markdown string and parses it server-side; the MTProto sendMessageTypingActionRichMessageDraft carries a blocks-based tg.RichMessage directly. botapi is MTProto-native, so it accepts the page-block tree (RichMessage{Blocks,Photos,Documents,RTL,Part}) without any content parser — more expressive than the string interface. Build blocks with the tg.PageBlock* types. This was the last deferred method; deferredMethods is now empty and conformance enforces the entire Bot API 10.1 surface (bar the 6 transport notApplicable). Hermetic test; lint clean.
1 parent a894dcf commit 1ecd431

3 files changed

Lines changed: 113 additions & 3 deletions

File tree

conformance_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,10 @@ var coveredByOtherMeans = map[string]string{
2020
// deferredMethods lists published methods that are feasible in principle but not
2121
// yet implemented, each blocked on a missing gotd/td RPC or unclear MTProto
2222
// mapping. They should move to an implementation as td gains support.
23-
var deferredMethods = map[string]string{
24-
"sendRichMessageDraft": "needs a tg.RichMessage PageBlock tree built from HTML/markdown, a content parser TDLib runs server-side",
25-
}
23+
//
24+
// Empty: every published method is implemented, covered by other means, or
25+
// not-applicable below.
26+
var deferredMethods = map[string]string{}
2627

2728
// notApplicableMethods lists published methods that do not apply to the
2829
// MTProto-native model and are intentionally not implemented.

send_draft.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,76 @@ func (b *Bot) SendMessageDraft(ctx context.Context, chat ChatID, draftID int64,
6767

6868
return nil
6969
}
70+
71+
// RichMessage is the structured content of a rich message, expressed as native
72+
// MTProto page blocks.
73+
//
74+
// The HTTP Bot API accepts a rich message only as an HTML or Markdown string and
75+
// parses it into this structure server-side. botapi is MTProto-native, so it
76+
// takes the page-block tree directly: no content parser is needed and the full
77+
// block vocabulary (paragraphs, headers, lists, embedded media, …) is available.
78+
// Build blocks with the tg.PageBlock* types; Bot.Raw exposes the same client.
79+
type RichMessage struct {
80+
// Blocks are the page blocks that make up the message body.
81+
Blocks []tg.PageBlockClass
82+
// Photos are photos referenced by the blocks.
83+
Photos []tg.PhotoClass
84+
// Documents are documents referenced by the blocks.
85+
Documents []tg.DocumentClass
86+
// RTL renders the message right-to-left.
87+
RTL bool
88+
// Part marks this as a partial segment of a longer streamed message.
89+
Part bool
90+
}
91+
92+
// toTg converts the rich message into its MTProto representation.
93+
func (r RichMessage) toTg() tg.RichMessage {
94+
out := tg.RichMessage{
95+
Blocks: r.Blocks,
96+
Photos: r.Photos,
97+
Documents: r.Documents,
98+
}
99+
if r.RTL {
100+
out.SetRtl(true)
101+
}
102+
103+
if r.Part {
104+
out.SetPart(true)
105+
}
106+
107+
return out
108+
}
109+
110+
// SendRichMessageDraft streams a partial rich message to a chat while the message
111+
// is being generated, so its members see an ephemeral preview. draftID is a
112+
// unique client-side identifier for the draft. Once generation finishes, persist
113+
// the result with a regular send.
114+
func (b *Bot) SendRichMessageDraft(ctx context.Context, chat ChatID, draftID int64, message RichMessage, opts ...DraftOption) error {
115+
var cfg draftConfig
116+
117+
for _, o := range opts {
118+
o(&cfg)
119+
}
120+
121+
peer, err := b.resolveInputPeer(ctx, chat)
122+
if err != nil {
123+
return err
124+
}
125+
126+
req := &tg.MessagesSetTypingRequest{
127+
Peer: peer,
128+
Action: &tg.SendMessageRichMessageDraftAction{
129+
RandomID: draftID,
130+
RichMessage: message.toTg(),
131+
},
132+
}
133+
if cfg.messageThreadID != 0 {
134+
req.SetTopMsgID(cfg.messageThreadID)
135+
}
136+
137+
if _, err := b.raw.MessagesSetTyping(ctx, req); err != nil {
138+
return asAPIError(err)
139+
}
140+
141+
return nil
142+
}

send_draft_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,39 @@ func TestSendMessageDraft(t *testing.T) {
3333
t.Fatalf("action = %#v", action)
3434
}
3535
}
36+
37+
func TestSendRichMessageDraft(t *testing.T) {
38+
inv := newMockInvoker()
39+
inv.reply(tg.MessagesSetTypingRequestTypeID, &tg.BoolTrue{})
40+
41+
message := RichMessage{
42+
Blocks: []tg.PageBlockClass{&tg.PageBlockParagraph{Text: &tg.TextPlain{Text: "partial"}}},
43+
RTL: true,
44+
Part: true,
45+
}
46+
47+
if err := newMockBot(inv).SendRichMessageDraft(context.Background(), userRef(10, 20), 1000, message); err != nil {
48+
t.Fatalf("SendRichMessageDraft: %v", err)
49+
}
50+
51+
var req tg.MessagesSetTypingRequest
52+
53+
inv.decode(t, tg.MessagesSetTypingRequestTypeID, &req)
54+
55+
action, ok := req.Action.(*tg.SendMessageRichMessageDraftAction)
56+
if !ok {
57+
t.Fatalf("action = %#v, want rich draft", req.Action)
58+
}
59+
60+
if action.RandomID != 1000 || len(action.RichMessage.Blocks) != 1 {
61+
t.Fatalf("action = %#v", action)
62+
}
63+
64+
if !action.RichMessage.GetRtl() {
65+
t.Fatal("rtl flag should be set")
66+
}
67+
68+
if _, ok := action.RichMessage.Blocks[0].(*tg.PageBlockParagraph); !ok {
69+
t.Fatalf("block 0 = %#v, want paragraph", action.RichMessage.Blocks[0])
70+
}
71+
}

0 commit comments

Comments
 (0)