Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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("<App ID>", "<App Secret>", lark.WithDomain(lark.DomainLark))
```

Or use the equivalent setter on an existing bot:

```go
bot := lark.NewChatBot("<App ID>", "<App Secret>")
bot.SetDomain(lark.DomainLark)
```

## Usage

### Bot Options

`NewChatBot` and `NewNotificationBot` accept variadic `BotOption`s for construction-time configuration:

```go
bot := lark.NewChatBot(
"<App ID>", "<App Secret>",
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.
Expand All @@ -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("<App ID>", "<App Secret>", 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:
Expand Down
45 changes: 42 additions & 3 deletions v2/README_zhCN.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,47 @@ func main() {

### 切换到 Lark 域名

go-lark 默认使用飞书 API 域名,我们需要调用`SetDomain`来切换到 Lark:
go-lark 默认使用飞书 API 域名,可以在创建 Bot 时使用 `WithDomain` 切换到 Lark:

```go
bot := lark.NewChatBot("<App ID>", "<App Secret>", lark.WithDomain(lark.DomainLark))
```

也可以在已有 Bot 上调用 setter:

```go
bot := lark.NewChatBot("<App ID>", "<App Secret>")
bot.SetDomain(lark.DomainLark)
```

## 用法

### Bot 选项

`NewChatBot` 与 `NewNotificationBot` 都支持变长参数 `BotOption`,便于在创建时一次性完成配置:

```go
bot := lark.NewChatBot(
"<App ID>", "<App Secret>",
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 默认自动开启鉴权更新功能。你可以直接调用任何接口。
Expand All @@ -107,11 +139,18 @@ resp, err := bot.GetTenantAccessTokenInternal(ctx)
// and we can now access the token value with `resp.TenantAccessToken`
```

关闭自动鉴权刷新:
关闭自动鉴权刷新(创建时):
```go
bot := lark.NewChatBot("<App ID>", "<App Secret>", lark.WithAutoRenew(false))
```

或运行时关闭:
```go
bot.SetAutoRenew(false)
```

当关闭自动刷新且未预设 tenant access token 时,调用 API 会返回 `ErrTenantAccessTokenEmpty`。

### 消息

简单消息可以以下接口直接通过:
Expand Down
1 change: 1 addition & 0 deletions v2/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 17 additions & 16 deletions v2/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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
}
Expand Down
70 changes: 66 additions & 4 deletions v2/lark.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -80,6 +138,10 @@ func NewNotificationBot(hookURL string) *Bot {
}
bot.tenantAccessToken.Store(TenantAccessToken{})

for _, opt := range opts {
opt(bot)
}

return bot
}

Expand Down
70 changes: 70 additions & 0 deletions v2/lark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading