Skip to content

Commit f886ecd

Browse files
ernadoclaude
andcommitted
feat: add BusinessContext handle
Bot.Business(connectionID) returns a *BusinessContext that scopes the business-account methods to a single connection, so the business_connection_id no longer has to be threaded through every call: Connection, SetName/SetBio/SetUsername, SetProfilePhoto/RemoveProfilePhoto, StarBalance, DeleteMessages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c64d097 commit f886ecd

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

business_context.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package botapi
2+
3+
import "context"
4+
5+
// BusinessContext is a handle scoped to a single business connection. It lets the
6+
// bot act on behalf of a connected business account without threading the
7+
// connection id through every call.
8+
//
9+
// Obtain one with Bot.Business, passing the business_connection_id received in
10+
// business updates (or returned by GetBusinessConnection). Its methods are thin
11+
// wrappers over the SetBusinessAccount* / GetBusinessAccount* / DeleteBusinessMessages
12+
// methods on *Bot, and require the same business bot rights.
13+
type BusinessContext struct {
14+
bot *Bot
15+
connectionID string
16+
}
17+
18+
// Business returns a handle that scopes business-account operations to the given
19+
// connection id.
20+
func (b *Bot) Business(connectionID string) *BusinessContext {
21+
return &BusinessContext{bot: b, connectionID: connectionID}
22+
}
23+
24+
// ConnectionID returns the business connection id this context is bound to.
25+
func (c *BusinessContext) ConnectionID() string { return c.connectionID }
26+
27+
// Connection returns information about this business connection.
28+
func (c *BusinessContext) Connection(ctx context.Context) (*BusinessConnection, error) {
29+
return c.bot.GetBusinessConnection(ctx, c.connectionID)
30+
}
31+
32+
// SetName changes the first and last name of the connected business account. The
33+
// bot must have the can_edit_name business bot right.
34+
func (c *BusinessContext) SetName(ctx context.Context, firstName, lastName string) error {
35+
return c.bot.SetBusinessAccountName(ctx, c.connectionID, firstName, lastName)
36+
}
37+
38+
// SetBio changes the bio of the connected business account. The bot must have the
39+
// can_edit_bio business bot right.
40+
func (c *BusinessContext) SetBio(ctx context.Context, bio string) error {
41+
return c.bot.SetBusinessAccountBio(ctx, c.connectionID, bio)
42+
}
43+
44+
// SetUsername changes the username of the connected business account. The bot
45+
// must have the can_edit_username business bot right.
46+
func (c *BusinessContext) SetUsername(ctx context.Context, username string) error {
47+
return c.bot.SetBusinessAccountUsername(ctx, c.connectionID, username)
48+
}
49+
50+
// SetProfilePhoto changes the profile photo of the connected business account.
51+
// When isPublic is true the photo becomes the public (fallback) photo. The bot
52+
// must have the can_edit_profile_photo business bot right.
53+
func (c *BusinessContext) SetProfilePhoto(ctx context.Context, photo InputProfilePhoto, isPublic bool) error {
54+
return c.bot.SetBusinessAccountProfilePhoto(ctx, c.connectionID, photo, isPublic)
55+
}
56+
57+
// RemoveProfilePhoto removes the profile photo of the connected business account.
58+
// When isPublic is true the public (fallback) photo is removed. The bot must have
59+
// the can_edit_profile_photo business bot right.
60+
func (c *BusinessContext) RemoveProfilePhoto(ctx context.Context, isPublic bool) error {
61+
return c.bot.RemoveBusinessAccountProfilePhoto(ctx, c.connectionID, isPublic)
62+
}
63+
64+
// StarBalance returns the Telegram Stars balance of the connected business
65+
// account. The bot must have the can_view_gifts_and_stars business bot right.
66+
func (c *BusinessContext) StarBalance(ctx context.Context) (StarAmount, error) {
67+
return c.bot.GetBusinessAccountStarBalance(ctx, c.connectionID)
68+
}
69+
70+
// DeleteMessages deletes messages on behalf of the connected business account.
71+
// The messages must all belong to the same chat.
72+
func (c *BusinessContext) DeleteMessages(ctx context.Context, messageIDs []int) error {
73+
return c.bot.DeleteBusinessMessages(ctx, c.connectionID, messageIDs)
74+
}

business_context_test.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package botapi
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/gotd/td/tg"
8+
)
9+
10+
func TestBusinessContextConnectionID(t *testing.T) {
11+
bc := newMockBot(newMockInvoker()).Business("bc9")
12+
13+
if bc.ConnectionID() != "bc9" {
14+
t.Fatalf("connection id = %q", bc.ConnectionID())
15+
}
16+
}
17+
18+
func TestBusinessContextSetNameScopesConnection(t *testing.T) {
19+
inv := newMockInvoker()
20+
inv.reply(tg.InvokeWithBusinessConnectionRequestTypeID, &tg.User{ID: 10})
21+
22+
if err := newMockBot(inv).Business("bc9").SetName(context.Background(), "First", "Last"); err != nil {
23+
t.Fatalf("SetName: %v", err)
24+
}
25+
26+
wrapper := tg.InvokeWithBusinessConnectionRequest{Query: &tg.AccountUpdateProfileRequest{}}
27+
28+
inv.decode(t, tg.InvokeWithBusinessConnectionRequestTypeID, &wrapper)
29+
30+
if wrapper.ConnectionID != "bc9" {
31+
t.Fatalf("connection id = %q", wrapper.ConnectionID)
32+
}
33+
34+
profile, ok := wrapper.Query.(*tg.AccountUpdateProfileRequest)
35+
if !ok {
36+
t.Fatalf("query = %#v", wrapper.Query)
37+
}
38+
39+
if first, _ := profile.GetFirstName(); first != "First" {
40+
t.Fatalf("first name = %q", first)
41+
}
42+
}
43+
44+
func TestBusinessContextStarBalance(t *testing.T) {
45+
inv := newMockInvoker()
46+
inv.reply(tg.InvokeWithBusinessConnectionRequestTypeID, &tg.PaymentsStarsStatus{
47+
Balance: &tg.StarsAmount{Amount: 7},
48+
})
49+
50+
got, err := newMockBot(inv).Business("bc9").StarBalance(context.Background())
51+
if err != nil {
52+
t.Fatalf("StarBalance: %v", err)
53+
}
54+
55+
if got.Amount != 7 {
56+
t.Fatalf("balance = %#v", got)
57+
}
58+
}
59+
60+
func TestBusinessContextDeleteMessages(t *testing.T) {
61+
inv := newMockInvoker()
62+
inv.reply(tg.InvokeWithBusinessConnectionRequestTypeID, &tg.MessagesAffectedMessages{})
63+
64+
if err := newMockBot(inv).Business("bc9").DeleteMessages(context.Background(), []int{1, 2}); err != nil {
65+
t.Fatalf("DeleteMessages: %v", err)
66+
}
67+
68+
wrapper := tg.InvokeWithBusinessConnectionRequest{Query: &tg.MessagesDeleteMessagesRequest{}}
69+
70+
inv.decode(t, tg.InvokeWithBusinessConnectionRequestTypeID, &wrapper)
71+
72+
del, ok := wrapper.Query.(*tg.MessagesDeleteMessagesRequest)
73+
if !ok || len(del.ID) != 2 {
74+
t.Fatalf("query = %#v", wrapper.Query)
75+
}
76+
}
77+
78+
func TestBusinessContextConnection(t *testing.T) {
79+
inv := newMockInvoker()
80+
inv.reply(tg.AccountGetBotBusinessConnectionRequestTypeID, &tg.Updates{
81+
Updates: []tg.UpdateClass{
82+
&tg.UpdateBotBusinessConnect{Connection: tg.BotBusinessConnection{ConnectionID: "bc9", UserID: 10}},
83+
},
84+
Users: []tg.UserClass{&tg.User{ID: 10, FirstName: "Biz"}},
85+
})
86+
87+
conn, err := newMockBot(inv).Business("bc9").Connection(context.Background())
88+
if err != nil {
89+
t.Fatalf("Connection: %v", err)
90+
}
91+
92+
if conn.ID != "bc9" {
93+
t.Fatalf("conn = %#v", conn)
94+
}
95+
96+
var req tg.AccountGetBotBusinessConnectionRequest
97+
98+
inv.decode(t, tg.AccountGetBotBusinessConnectionRequestTypeID, &req)
99+
100+
if req.ConnectionID != "bc9" {
101+
t.Fatalf("requested connection id = %q", req.ConnectionID)
102+
}
103+
}
104+
105+
func TestBusinessContextRemoveProfilePhoto(t *testing.T) {
106+
inv := newMockInvoker()
107+
inv.reply(tg.InvokeWithBusinessConnectionRequestTypeID, &tg.PhotosPhoto{Photo: &tg.PhotoEmpty{}})
108+
109+
if err := newMockBot(inv).Business("bc9").RemoveProfilePhoto(context.Background(), false); err != nil {
110+
t.Fatalf("RemoveProfilePhoto: %v", err)
111+
}
112+
113+
wrapper := tg.InvokeWithBusinessConnectionRequest{Query: &tg.PhotosUpdateProfilePhotoRequest{}}
114+
115+
inv.decode(t, tg.InvokeWithBusinessConnectionRequestTypeID, &wrapper)
116+
117+
if wrapper.ConnectionID != "bc9" {
118+
t.Fatalf("connection id = %q", wrapper.ConnectionID)
119+
}
120+
}

0 commit comments

Comments
 (0)