Skip to content

Commit f7f55cb

Browse files
ernadoclaude
andcommitted
feat(send): convert tg.Message to Bot API Message
convertMessage resolves the chat and sender via the peer manager and fills the core Message fields: text, entities, reply target, author signature, edit date and inline reply markup. Pure helpers userFromTgUser, chatFromPeer and inlineKeyboardFromTg do the field-level translation. Richer media mapping follows with the media work. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 69f96a1 commit f7f55cb

2 files changed

Lines changed: 207 additions & 0 deletions

File tree

convert.go

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/telegram/peers"
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
// userFromTgUser converts a raw tg.User into a Bot API User. Pure: optional
11+
// fields are taken as their zero values when absent.
12+
func userFromTgUser(u *tg.User) User {
13+
return User{
14+
ID: u.ID,
15+
IsBot: u.Bot,
16+
FirstName: u.FirstName,
17+
LastName: u.LastName,
18+
Username: u.Username,
19+
LanguageCode: u.LangCode,
20+
IsPremium: u.Premium,
21+
}
22+
}
23+
24+
// chatFromPeer builds a Bot API Chat from a resolved gotd peer.
25+
//
26+
// The default arm keeps the switch closed against future peer kinds
27+
// (default-signifies-exhaustive).
28+
func chatFromPeer(p peers.Peer) Chat {
29+
switch p := p.(type) {
30+
case peers.User:
31+
c := Chat{ID: int64(p.TDLibPeerID()), Type: ChatTypePrivate}
32+
if v, ok := p.Username(); ok {
33+
c.Username = v
34+
}
35+
if v, ok := p.FirstName(); ok {
36+
c.FirstName = v
37+
}
38+
if v, ok := p.LastName(); ok {
39+
c.LastName = v
40+
}
41+
return c
42+
case peers.Channel:
43+
c := Chat{ID: int64(p.TDLibPeerID()), Title: p.VisibleName(), Type: ChatTypeSupergroup}
44+
if _, ok := p.ToBroadcast(); ok {
45+
c.Type = ChatTypeChannel
46+
}
47+
if v, ok := p.Username(); ok {
48+
c.Username = v
49+
}
50+
return c
51+
case peers.Chat:
52+
return Chat{ID: int64(p.TDLibPeerID()), Type: ChatTypeGroup, Title: p.VisibleName()}
53+
default:
54+
return Chat{ID: int64(p.TDLibPeerID())}
55+
}
56+
}
57+
58+
// inlineKeyboardFromTg converts an MTProto inline markup back into a Bot API
59+
// InlineKeyboardMarkup. Pure.
60+
func inlineKeyboardFromTg(mkp *tg.ReplyInlineMarkup) *InlineKeyboardMarkup {
61+
rows := make([][]InlineKeyboardButton, len(mkp.Rows))
62+
for i, row := range mkp.Rows {
63+
buttons := make([]InlineKeyboardButton, len(row.Buttons))
64+
for j, b := range row.Buttons {
65+
btn := InlineKeyboardButton{Text: b.GetText()}
66+
switch b := b.(type) {
67+
case *tg.KeyboardButtonURL:
68+
btn.URL = b.URL
69+
case *tg.KeyboardButtonCallback:
70+
btn.CallbackData = string(b.Data)
71+
case *tg.KeyboardButtonBuy:
72+
btn.Pay = true
73+
case *tg.KeyboardButtonSwitchInline:
74+
q := b.Query
75+
if b.SamePeer {
76+
btn.SwitchInlineQueryCurrentChat = &q
77+
} else {
78+
btn.SwitchInlineQuery = &q
79+
}
80+
case *tg.KeyboardButtonURLAuth:
81+
// login_url buttons are represented as ordinary url buttons.
82+
btn.URL = b.URL
83+
}
84+
buttons[j] = btn
85+
}
86+
rows[i] = buttons
87+
}
88+
return &InlineKeyboardMarkup{InlineKeyboard: rows}
89+
}
90+
91+
// chatByPeer resolves a tg.PeerClass to a Bot API Chat via the peer manager.
92+
func (b *Bot) chatByPeer(ctx context.Context, p tg.PeerClass) (Chat, error) {
93+
peer, err := b.peers.ResolvePeer(ctx, p)
94+
if err != nil {
95+
return Chat{}, asAPIError(err)
96+
}
97+
return chatFromPeer(peer), nil
98+
}
99+
100+
// fillFrom sets Message.From (for users) or Message.SenderChat (for chats and
101+
// channels) from a tg sender peer.
102+
func (b *Bot) fillFrom(ctx context.Context, from tg.PeerClass, m *Message) error {
103+
switch from := from.(type) {
104+
case *tg.PeerUser:
105+
u, err := b.peers.GetUser(ctx, &tg.InputUser{UserID: from.UserID})
106+
if err != nil {
107+
return asAPIError(err)
108+
}
109+
user := userFromTgUser(u.Raw())
110+
m.From = &user
111+
case *tg.PeerChat, *tg.PeerChannel:
112+
chat, err := b.chatByPeer(ctx, from)
113+
if err != nil {
114+
return err
115+
}
116+
m.SenderChat = &chat
117+
}
118+
return nil
119+
}
120+
121+
// convertMessage translates a tg.Message into a Bot API Message, resolving the
122+
// chat and sender via the peer manager. It fills the core fields (text,
123+
// entities, reply target, inline markup); richer media mapping arrives with the
124+
// media send/receive work.
125+
func (b *Bot) convertMessage(ctx context.Context, m *tg.Message) (*Message, error) {
126+
chat, err := b.chatByPeer(ctx, m.PeerID)
127+
if err != nil {
128+
return nil, err
129+
}
130+
131+
r := &Message{
132+
MessageID: m.ID,
133+
Date: m.Date,
134+
Chat: chat,
135+
HasProtectedContent: m.Noforwards,
136+
}
137+
if v, ok := m.GetEditDate(); ok {
138+
r.EditDate = v
139+
}
140+
if v, ok := m.GetPostAuthor(); ok {
141+
r.AuthorSignature = v
142+
}
143+
144+
if m.Out {
145+
if self, selfErr := b.peers.Self(ctx); selfErr == nil {
146+
user := userFromTgUser(self.Raw())
147+
r.From = &user
148+
}
149+
} else if from, ok := m.GetFromID(); ok {
150+
if err := b.fillFrom(ctx, from, r); err != nil {
151+
return nil, err
152+
}
153+
}
154+
155+
if rh, ok := m.ReplyTo.(*tg.MessageReplyHeader); ok && rh.ReplyToMsgID != 0 {
156+
r.ReplyToMessage = &Message{MessageID: rh.ReplyToMsgID}
157+
}
158+
159+
if m.Message != "" {
160+
r.Text = m.Message
161+
}
162+
if len(m.Entities) > 0 {
163+
r.Entities = entitiesFromTg(m.Entities)
164+
}
165+
if mkp, ok := m.ReplyMarkup.(*tg.ReplyInlineMarkup); ok {
166+
r.ReplyMarkup = inlineKeyboardFromTg(mkp)
167+
}
168+
169+
return r, nil
170+
}

convert_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package botapi
2+
3+
import (
4+
"testing"
5+
6+
"github.com/gotd/td/tg"
7+
)
8+
9+
func TestUserFromTgUser(t *testing.T) {
10+
u := &tg.User{ID: 42, Bot: true, FirstName: "Ada", LastName: "L", Username: "ada", LangCode: "en"}
11+
u.SetPremium(true)
12+
got := userFromTgUser(u)
13+
want := User{ID: 42, IsBot: true, FirstName: "Ada", LastName: "L", Username: "ada", LanguageCode: "en", IsPremium: true}
14+
if got != want {
15+
t.Fatalf("got %+v want %+v", got, want)
16+
}
17+
}
18+
19+
func TestInlineKeyboardFromTg(t *testing.T) {
20+
mkp := &tg.ReplyInlineMarkup{Rows: []tg.KeyboardButtonRow{{Buttons: []tg.KeyboardButtonClass{
21+
&tg.KeyboardButtonURL{Text: "site", URL: "https://example.com"},
22+
&tg.KeyboardButtonCallback{Text: "ok", Data: []byte("ok:1")},
23+
&tg.KeyboardButtonSwitchInline{Text: "here", Query: "q", SamePeer: true},
24+
}}}}
25+
26+
got := inlineKeyboardFromTg(mkp)
27+
if len(got.InlineKeyboard) != 1 || len(got.InlineKeyboard[0]) != 3 {
28+
t.Fatalf("unexpected shape: %+v", got.InlineKeyboard)
29+
}
30+
row := got.InlineKeyboard[0]
31+
if row[0].URL != "https://example.com" || row[1].CallbackData != "ok:1" {
32+
t.Fatalf("url/callback lost: %+v", row)
33+
}
34+
if row[2].SwitchInlineQueryCurrentChat == nil || *row[2].SwitchInlineQueryCurrentChat != "q" {
35+
t.Fatalf("same-peer switch-inline lost: %+v", row[2])
36+
}
37+
}

0 commit comments

Comments
 (0)