Skip to content

Commit a6e2d39

Browse files
ernadoclaude
andcommitted
feat: add business-account and managed-bot methods
Business account (on behalf of a connected account via the new invokeWithBusinessConnection wrapper, threaded through a Bot.invoker): GetBusinessConnection (account.getBotBusinessConnection), SetBusinessAccountName/ Bio/Username (account.updateProfile/updateUsername), GetBusinessAccountStarBalance (payments.getStarsStatus), DeleteBusinessMessages (messages.deleteMessages). Managed bot (direct bots.* RPCs): Get/SetManagedBotAccessSettings (bots.get/editAccessSettings), Get/ReplaceManagedBotToken (bots.exportBotToken). Adds BusinessConnection, BusinessBotRights and ManagedBotAccessSettings types. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 5ff87ae commit a6e2d39

7 files changed

Lines changed: 584 additions & 10 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,6 @@ state/*.bbolt
3535
# Subscriber store written by the background example.
3636
subscribers.json
3737
subscribers.json.tmp
38+
39+
# Local parity status doc (not committed).
40+
parity.md

bot.go

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,14 @@ type Bot struct {
2424

2525
client *telegram.Client
2626
raw *tg.Client
27-
sender *message.Sender
28-
peers *peers.Manager
29-
gaps *updates.Manager
30-
disp tg.UpdateDispatcher
27+
// invoker is the low-level RPC entry point (client.API() hides its invoker),
28+
// used to send invokeWith* wrappers such as invokeWithBusinessConnection that
29+
// *tg.Client exposes no generated helper for.
30+
invoker tg.Invoker
31+
sender *message.Sender
32+
peers *peers.Manager
33+
gaps *updates.Manager
34+
disp tg.UpdateDispatcher
3135

3236
router router
3337

@@ -107,6 +111,7 @@ func New(token string, opt Options) (*Bot, error) {
107111
log: opt.Logger,
108112
client: client,
109113
raw: client.API(),
114+
invoker: client,
110115
sender: message.NewSender(client.API()),
111116
peers: pm,
112117
gaps: gaps,

business.go

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
6+
"github.com/gotd/td/bin"
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
// BusinessBotRights lists the rights a business bot has over the connected
11+
// business account.
12+
type BusinessBotRights struct {
13+
CanReply bool `json:"can_reply,omitempty"`
14+
CanReadMessages bool `json:"can_read_messages,omitempty"`
15+
CanDeleteSentMessages bool `json:"can_delete_sent_messages,omitempty"`
16+
CanDeleteAllMessages bool `json:"can_delete_all_messages,omitempty"`
17+
CanEditName bool `json:"can_edit_name,omitempty"`
18+
CanEditBio bool `json:"can_edit_bio,omitempty"`
19+
CanEditProfilePhoto bool `json:"can_edit_profile_photo,omitempty"`
20+
CanEditUsername bool `json:"can_edit_username,omitempty"`
21+
CanChangeGiftSettings bool `json:"can_change_gift_settings,omitempty"`
22+
CanViewGiftsAndStars bool `json:"can_view_gifts_and_stars,omitempty"`
23+
CanConvertGiftsToStars bool `json:"can_convert_gifts_to_stars,omitempty"`
24+
CanTransferAndUpgradeGifts bool `json:"can_transfer_and_upgrade_gifts,omitempty"`
25+
CanTransferStars bool `json:"can_transfer_stars,omitempty"`
26+
CanManageStories bool `json:"can_manage_stories,omitempty"`
27+
}
28+
29+
// businessBotRightsFromTg converts MTProto business bot rights to the Bot API
30+
// representation.
31+
func businessBotRightsFromTg(r tg.BusinessBotRights) BusinessBotRights {
32+
return BusinessBotRights{
33+
CanReply: r.Reply,
34+
CanReadMessages: r.ReadMessages,
35+
CanDeleteSentMessages: r.DeleteSentMessages,
36+
CanDeleteAllMessages: r.DeleteReceivedMessages,
37+
CanEditName: r.EditName,
38+
CanEditBio: r.EditBio,
39+
CanEditProfilePhoto: r.EditProfilePhoto,
40+
CanEditUsername: r.EditUsername,
41+
CanChangeGiftSettings: r.ChangeGiftSettings,
42+
CanViewGiftsAndStars: r.ViewGifts,
43+
CanConvertGiftsToStars: r.SellGifts,
44+
CanTransferAndUpgradeGifts: r.TransferAndUpgradeGifts,
45+
CanTransferStars: r.TransferStars,
46+
CanManageStories: r.ManageStories,
47+
}
48+
}
49+
50+
// BusinessConnection describes the connection of the bot with a business
51+
// account.
52+
type BusinessConnection struct {
53+
// ID is the unique identifier of the business connection.
54+
ID string `json:"id"`
55+
// User is the business account user that connected to the bot.
56+
User User `json:"user"`
57+
// UserChatID is the identifier of the private chat with the user.
58+
UserChatID int64 `json:"user_chat_id"`
59+
// Date is the Unix time when the connection was established.
60+
Date int `json:"date"`
61+
// Rights are the rights of the business bot.
62+
Rights *BusinessBotRights `json:"rights,omitempty"`
63+
// IsEnabled reports whether the connection is active.
64+
IsEnabled bool `json:"is_enabled"`
65+
}
66+
67+
// invokeBusiness sends an inner RPC on behalf of a connected business account,
68+
// wrapping it in invokeWithBusinessConnection. *tg.Client has no generated
69+
// helper for this wrapper, so it goes through the raw invoker.
70+
func (b *Bot) invokeBusiness(ctx context.Context, connectionID string, query bin.Object, output bin.Decoder) error {
71+
return b.invoker.Invoke(ctx, &tg.InvokeWithBusinessConnectionRequest{
72+
ConnectionID: connectionID,
73+
Query: query,
74+
}, output)
75+
}
76+
77+
// GetBusinessConnection returns information about the connection of the bot with
78+
// a business account by the connection id received in updates.
79+
func (b *Bot) GetBusinessConnection(ctx context.Context, businessConnectionID string) (*BusinessConnection, error) {
80+
res, err := b.raw.AccountGetBotBusinessConnection(ctx, businessConnectionID)
81+
if err != nil {
82+
return nil, asAPIError(err)
83+
}
84+
85+
updates, users := updatesAndUsers(res)
86+
87+
for _, upd := range updates {
88+
connect, ok := upd.(*tg.UpdateBotBusinessConnect)
89+
if !ok {
90+
continue
91+
}
92+
93+
return b.businessConnectionFromTg(connect.Connection, users), nil
94+
}
95+
96+
return nil, &Error{Code: 400, Description: "Bad Request: business connection not found"}
97+
}
98+
99+
// businessConnectionFromTg converts an MTProto business connection into the Bot
100+
// API type, resolving the connected user from the update's users.
101+
func (b *Bot) businessConnectionFromTg(c tg.BotBusinessConnection, users map[int64]*tg.User) *BusinessConnection {
102+
out := &BusinessConnection{
103+
ID: c.ConnectionID,
104+
UserChatID: c.UserID,
105+
Date: c.Date,
106+
IsEnabled: !c.Disabled,
107+
}
108+
109+
if u, ok := users[c.UserID]; ok {
110+
out.User = userFromTgUser(u)
111+
} else {
112+
out.User = User{ID: c.UserID}
113+
}
114+
115+
rights := businessBotRightsFromTg(c.Rights)
116+
117+
out.Rights = &rights
118+
119+
return out
120+
}
121+
122+
// SetBusinessAccountName changes the first and last name of a managed business
123+
// account. The bot must have the can_edit_name business bot right.
124+
func (b *Bot) SetBusinessAccountName(ctx context.Context, businessConnectionID, firstName, lastName string) error {
125+
req := &tg.AccountUpdateProfileRequest{}
126+
req.SetFirstName(firstName)
127+
req.SetLastName(lastName)
128+
129+
return b.invokeBusiness(ctx, businessConnectionID, req, &tg.UserBox{})
130+
}
131+
132+
// SetBusinessAccountBio changes the bio of a managed business account. The bot
133+
// must have the can_edit_bio business bot right.
134+
func (b *Bot) SetBusinessAccountBio(ctx context.Context, businessConnectionID, bio string) error {
135+
req := &tg.AccountUpdateProfileRequest{}
136+
req.SetAbout(bio)
137+
138+
return b.invokeBusiness(ctx, businessConnectionID, req, &tg.UserBox{})
139+
}
140+
141+
// SetBusinessAccountUsername changes the username of a managed business account.
142+
// The bot must have the can_edit_username business bot right.
143+
func (b *Bot) SetBusinessAccountUsername(ctx context.Context, businessConnectionID, username string) error {
144+
return b.invokeBusiness(ctx, businessConnectionID, &tg.AccountUpdateUsernameRequest{Username: username}, &tg.UserBox{})
145+
}
146+
147+
// GetBusinessAccountStarBalance returns the amount of Telegram Stars owned by a
148+
// managed business account. The bot must have the can_view_gifts_and_stars
149+
// business bot right.
150+
func (b *Bot) GetBusinessAccountStarBalance(ctx context.Context, businessConnectionID string) (StarAmount, error) {
151+
var status tg.PaymentsStarsStatus
152+
153+
err := b.invokeBusiness(ctx, businessConnectionID, &tg.PaymentsGetStarsStatusRequest{
154+
Peer: &tg.InputPeerSelf{},
155+
}, &status)
156+
if err != nil {
157+
return StarAmount{}, asAPIError(err)
158+
}
159+
160+
amount, ok := status.Balance.(*tg.StarsAmount)
161+
if !ok {
162+
return StarAmount{}, &Error{Code: 500, Description: "Internal Server Error: unexpected stars balance"}
163+
}
164+
165+
return StarAmount{Amount: int(amount.Amount), NanostarAmount: amount.Nanos}, nil
166+
}
167+
168+
// DeleteBusinessMessages deletes messages on behalf of a managed business
169+
// account. The messages must all belong to the same chat. The bot must have the
170+
// can_delete_sent_messages right (for its own messages) or can_delete_all_messages
171+
// right.
172+
func (b *Bot) DeleteBusinessMessages(ctx context.Context, businessConnectionID string, messageIDs []int) error {
173+
if len(messageIDs) == 0 {
174+
return nil
175+
}
176+
177+
var affected tg.MessagesAffectedMessages
178+
179+
err := b.invokeBusiness(ctx, businessConnectionID, &tg.MessagesDeleteMessagesRequest{
180+
Revoke: true,
181+
ID: messageIDs,
182+
}, &affected)
183+
if err != nil {
184+
return asAPIError(err)
185+
}
186+
187+
return nil
188+
}
189+
190+
// updatesAndUsers unpacks the update list and a user-by-id index from an
191+
// Updates response.
192+
func updatesAndUsers(resp tg.UpdatesClass) (updates []tg.UpdateClass, users map[int64]*tg.User) {
193+
switch u := resp.(type) {
194+
case *tg.Updates:
195+
return u.Updates, usersByID(u.Users)
196+
case *tg.UpdatesCombined:
197+
return u.Updates, usersByID(u.Users)
198+
default:
199+
return nil, nil
200+
}
201+
}

0 commit comments

Comments
 (0)