From fcd7e4da92d3733660876abeaad679a67241e5d6 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Thu, 14 May 2026 20:56:00 +0800 Subject: [PATCH 1/3] fix(http): second instead of nanosecond [by AI] --- v2/http.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/v2/http.go b/v2/http.go index 4953f77..78641c8 100644 --- a/v2/http.go +++ b/v2/http.go @@ -39,7 +39,7 @@ func (bot *Bot) loadAndRenewToken(ctx context.Context) (string, error) { return "", err } now := time.Now() - expire := time.Duration(tacResp.Expire - 10) + expire := time.Duration(tacResp.Expire-10) * time.Second eta := now.Add(expire) token := TenantAccessToken{ TenantAccessToken: tacResp.TenantAccessToken, From 5736f8793ad742834aadb8fdcb752a041252357e Mon Sep 17 00:00:00 2001 From: David Zhang Date: Thu, 14 May 2026 20:57:23 +0800 Subject: [PATCH 2/3] fix(auth): returns error if autoRenew is not enabled and tenant access token is empty [by AI] --- v2/error.go | 1 + v2/http.go | 33 +++++++++++++++++---------------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/v2/error.go b/v2/error.go index c0f5b0b..0a2b9ee 100644 --- a/v2/error.go +++ b/v2/error.go @@ -18,6 +18,7 @@ var ( ErrInvalidReceiveID = errors.New("Invalid receive ID") ErrEventTypeNotMatch = errors.New("Event type not match") ErrMessageType = errors.New("Message type error") + ErrTenantAccessTokenEmpty = errors.New("Tenant access token is empty and auto-renew is disabled") ) // APIError constructs an error with given response diff --git a/v2/http.go b/v2/http.go index 78641c8..e76d6cb 100644 --- a/v2/http.go +++ b/v2/http.go @@ -33,23 +33,24 @@ func (bot *Bot) loadAndRenewToken(ctx context.Context) (string, error) { tenantAccessToken := token.TenantAccessToken if !ok || token.TenantAccessToken == "" || (token.EstimatedExpireAt != nil && now.After(*token.EstimatedExpireAt)) { // renew token - if bot.autoRenew { - tacResp, err := bot.GetTenantAccessTokenInternal(ctx) - if err != nil { - return "", err - } - now := time.Now() - expire := time.Duration(tacResp.Expire-10) * time.Second - eta := now.Add(expire) - token := TenantAccessToken{ - TenantAccessToken: tacResp.TenantAccessToken, - Expire: expire, - LastUpdatedAt: &now, - EstimatedExpireAt: &eta, - } - bot.tenantAccessToken.Store(token) - tenantAccessToken = tacResp.TenantAccessToken + if !bot.autoRenew { + return "", ErrTenantAccessTokenEmpty } + tacResp, err := bot.GetTenantAccessTokenInternal(ctx) + if err != nil { + return "", err + } + now := time.Now() + expire := time.Duration(tacResp.Expire-10) * time.Second + eta := now.Add(expire) + token := TenantAccessToken{ + TenantAccessToken: tacResp.TenantAccessToken, + Expire: expire, + LastUpdatedAt: &now, + EstimatedExpireAt: &eta, + } + bot.tenantAccessToken.Store(token) + tenantAccessToken = tacResp.TenantAccessToken } return tenantAccessToken, nil } From e77526c34e493a36cd2f7d153ec8ad6066093ec5 Mon Sep 17 00:00:00 2001 From: David Zhang Date: Thu, 14 May 2026 21:10:23 +0800 Subject: [PATCH 3/3] feat(bot): add options --- v2/README.md | 45 ++++++++++++++++++++++++++++-- v2/README_zhCN.md | 45 ++++++++++++++++++++++++++++-- v2/lark.go | 70 ++++++++++++++++++++++++++++++++++++++++++++--- v2/lark_test.go | 70 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 220 insertions(+), 10 deletions(-) diff --git a/v2/README.md b/v2/README.md index 43a3d38..243188e 100644 --- a/v2/README.md +++ b/v2/README.md @@ -92,15 +92,47 @@ Feishu/Lark API offers more features, please refers to [Usage](#usage) for furth ### Switch to Lark Endpoints -The default API endpoints are for Feishu, in order to switch to Lark, we should use `SetDomain`: +The default API endpoints are for Feishu, in order to switch to Lark, we may pass `WithDomain` at construction: + +```go +bot := lark.NewChatBot("", "", lark.WithDomain(lark.DomainLark)) +``` + +Or use the equivalent setter on an existing bot: ```go -bot := lark.NewChatBot("", "") bot.SetDomain(lark.DomainLark) ``` ## Usage +### Bot Options + +`NewChatBot` and `NewNotificationBot` accept variadic `BotOption`s for construction-time configuration: + +```go +bot := lark.NewChatBot( + "", "", + lark.WithDomain(lark.DomainLark), + lark.WithAutoRenew(false), + lark.WithUserIDType(lark.UIDOpenID), + lark.WithHTTPClient(myHTTPClient), + lark.WithLogger(myLogger), +) +``` + +Available options: + +| Option | Description | +| ------------------------ | -------------------------------------------------------- | +| `WithHTTPClient` | Custom `HTTPClient` implementation | +| `WithLogger` | Custom `LogWrapper` | +| `WithDomain` | API domain (`DomainFeishu` / `DomainLark`) | +| `WithAutoRenew` | Toggle tenant access token auto-renew (default `true`) | +| `WithTenantAccessToken` | Pre-set a tenant access token | +| `WithUserIDType` | Default user ID type for chat APIs | +| `WithWebhook` | Webhook URL (mainly for `NotificationBot`) | + ### Auth go-lark v2 triggers auto-renewable retrieval of tenant access token by default. @@ -113,11 +145,18 @@ resp, err := bot.GetTenantAccessTokenInternal(ctx) // and we can now access the token value with `resp.TenantAccessToken` ``` -Switch off auto renew: +Switch off auto renew (at construction): +```go +bot := lark.NewChatBot("", "", lark.WithAutoRenew(false)) +``` + +Or at runtime: ```go bot.SetAutoRenew(false) ``` +When auto-renew is disabled and no tenant access token has been set, API calls will return `ErrTenantAccessTokenEmpty`. + ### Messaging For Chat Bot, we can send simple messages with the following method: diff --git a/v2/README_zhCN.md b/v2/README_zhCN.md index 27aa68a..9e03e52 100644 --- a/v2/README_zhCN.md +++ b/v2/README_zhCN.md @@ -87,15 +87,47 @@ func main() { ### 切换到 Lark 域名 -go-lark 默认使用飞书 API 域名,我们需要调用`SetDomain`来切换到 Lark: +go-lark 默认使用飞书 API 域名,可以在创建 Bot 时使用 `WithDomain` 切换到 Lark: + +```go +bot := lark.NewChatBot("", "", lark.WithDomain(lark.DomainLark)) +``` + +也可以在已有 Bot 上调用 setter: ```go -bot := lark.NewChatBot("", "") bot.SetDomain(lark.DomainLark) ``` ## 用法 +### Bot 选项 + +`NewChatBot` 与 `NewNotificationBot` 都支持变长参数 `BotOption`,便于在创建时一次性完成配置: + +```go +bot := lark.NewChatBot( + "", "", + lark.WithDomain(lark.DomainLark), + lark.WithAutoRenew(false), + lark.WithUserIDType(lark.UIDOpenID), + lark.WithHTTPClient(myHTTPClient), + lark.WithLogger(myLogger), +) +``` + +可用选项: + +| 选项 | 说明 | +| ------------------------ | ---------------------------------------------------------- | +| `WithHTTPClient` | 自定义 `HTTPClient` 实现 | +| `WithLogger` | 自定义 `LogWrapper` 日志器 | +| `WithDomain` | 设置 API 域名(`DomainFeishu` / `DomainLark`) | +| `WithAutoRenew` | 切换 tenant access token 自动刷新(默认 `true`) | +| `WithTenantAccessToken` | 预先设置 tenant access token | +| `WithUserIDType` | 设置默认的 user ID 类型 | +| `WithWebhook` | 设置 webhook URL(主要用于 `NotificationBot`) | + ### 鉴权 go-lark v2 默认自动开启鉴权更新功能。你可以直接调用任何接口。 @@ -107,11 +139,18 @@ resp, err := bot.GetTenantAccessTokenInternal(ctx) // and we can now access the token value with `resp.TenantAccessToken` ``` -关闭自动鉴权刷新: +关闭自动鉴权刷新(创建时): +```go +bot := lark.NewChatBot("", "", lark.WithAutoRenew(false)) +``` + +或运行时关闭: ```go bot.SetAutoRenew(false) ``` +当关闭自动刷新且未预设 tenant access token 时,调用 API 会返回 `ErrTenantAccessTokenEmpty`。 + ### 消息 简单消息可以以下接口直接通过: diff --git a/v2/lark.go b/v2/lark.go index b7488bb..ea8cd8a 100644 --- a/v2/lark.go +++ b/v2/lark.go @@ -54,8 +54,61 @@ type TenantAccessToken struct { EstimatedExpireAt *time.Time } -// NewChatBot with appID and appSecret -func NewChatBot(appID, appSecret string) *Bot { +// BotOption configures a Bot. +type BotOption func(*Bot) + +// WithHTTPClient sets a custom HTTP client. +func WithHTTPClient(c HTTPClient) BotOption { + return func(bot *Bot) { + bot.client = c + } +} + +// WithLogger sets a custom logger. +func WithLogger(logger LogWrapper) BotOption { + return func(bot *Bot) { + bot.logger = logger + } +} + +// WithDomain sets the API domain (e.g. DomainFeishu, DomainLark). +func WithDomain(domain string) BotOption { + return func(bot *Bot) { + bot.domain = domain + } +} + +// WithAutoRenew toggles tenant access token auto renew. +func WithAutoRenew(autoRenew bool) BotOption { + return func(bot *Bot) { + bot.autoRenew = autoRenew + } +} + +// WithTenantAccessToken sets an initial tenant access token. +func WithTenantAccessToken(t TenantAccessToken) BotOption { + return func(bot *Bot) { + bot.tenantAccessToken.Store(t) + } +} + +// WithUserIDType sets the default user ID type used by chat APIs. +func WithUserIDType(userIDType string) BotOption { + return func(bot *Bot) { + bot.userIDType = userIDType + } +} + +// WithWebhook sets the webhook URL (mainly for NotificationBot). +func WithWebhook(hookURL string) BotOption { + return func(bot *Bot) { + bot.webhook = hookURL + } +} + +// NewChatBot with appID and appSecret. +// Additional configuration can be supplied via BotOption. +func NewChatBot(appID, appSecret string, opts ...BotOption) *Bot { bot := &Bot{ botType: ChatBot, appID: appID, @@ -67,11 +120,16 @@ func NewChatBot(appID, appSecret string) *Bot { bot.autoRenew = true bot.tenantAccessToken.Store(TenantAccessToken{}) + for _, opt := range opts { + opt(bot) + } + return bot } -// NewNotificationBot with URL -func NewNotificationBot(hookURL string) *Bot { +// NewNotificationBot with URL. +// Additional configuration can be supplied via BotOption. +func NewNotificationBot(hookURL string, opts ...BotOption) *Bot { bot := &Bot{ botType: NotificationBot, webhook: hookURL, @@ -80,6 +138,10 @@ func NewNotificationBot(hookURL string) *Bot { } bot.tenantAccessToken.Store(TenantAccessToken{}) + for _, opt := range opts { + opt(bot) + } + return bot } diff --git a/v2/lark_test.go b/v2/lark_test.go index d56c802..90b2585 100644 --- a/v2/lark_test.go +++ b/v2/lark_test.go @@ -155,3 +155,73 @@ func TestTenantAccessToken(t *testing.T) { }) assert.Equal(t, "test", bot.TenantAccessToken()) } + +func TestWithHTTPClient(t *testing.T) { + custom := newDefaultClient() + bot := NewChatBot(testAppID, testAppSecret, WithHTTPClient(custom)) + assert.Equal(t, custom, bot.client) +} + +func TestWithLogger(t *testing.T) { + logger := initDefaultLogger() + bot := NewChatBot(testAppID, testAppSecret, WithLogger(logger)) + assert.Equal(t, logger, bot.logger) +} + +func TestWithDomain(t *testing.T) { + bot := NewChatBot(testAppID, testAppSecret, WithDomain(DomainLark)) + assert.Equal(t, DomainLark, bot.domain) +} + +func TestWithAutoRenew(t *testing.T) { + bot := NewChatBot(testAppID, testAppSecret, WithAutoRenew(false)) + assert.False(t, bot.autoRenew) + + bot = NewChatBot(testAppID, testAppSecret, WithAutoRenew(true)) + assert.True(t, bot.autoRenew) +} + +func TestWithTenantAccessToken(t *testing.T) { + token := TenantAccessToken{TenantAccessToken: "preset"} + bot := NewChatBot(testAppID, testAppSecret, WithTenantAccessToken(token)) + assert.Equal(t, "preset", bot.TenantAccessToken()) +} + +func TestWithUserIDType(t *testing.T) { + bot := NewChatBot(testAppID, testAppSecret, WithUserIDType(UIDOpenID)) + assert.Equal(t, UIDOpenID, bot.userIDType) +} + +func TestWithWebhook(t *testing.T) { + bot := NewNotificationBot("initial", WithWebhook("override")) + assert.Equal(t, "override", bot.webhook) +} + +func TestNewChatBotWithMultipleOptions(t *testing.T) { + logger := initDefaultLogger() + client := newDefaultClient() + bot := NewChatBot( + testAppID, testAppSecret, + WithDomain(DomainLark), + WithAutoRenew(false), + WithUserIDType(UIDOpenID), + WithHTTPClient(client), + WithLogger(logger), + ) + assert.Equal(t, DomainLark, bot.domain) + assert.False(t, bot.autoRenew) + assert.Equal(t, UIDOpenID, bot.userIDType) + assert.Equal(t, client, bot.client) + assert.Equal(t, logger, bot.logger) +} + +func TestNewNotificationBotWithOptions(t *testing.T) { + logger := initDefaultLogger() + bot := NewNotificationBot( + "hook", + WithLogger(logger), + ) + assert.Equal(t, "hook", bot.webhook) + assert.Equal(t, logger, bot.logger) + assert.Equal(t, NotificationBot, bot.botType) +}