Skip to content

Commit 1e8532a

Browse files
committed
refactor: http lib with context
1 parent 9ada710 commit 1e8532a

File tree

10 files changed

+631
-0
lines changed

10 files changed

+631
-0
lines changed

v2/error.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package lark
2+
3+
import "errors"
4+
5+
// Errors
6+
var (
7+
ErrBotTypeError = errors.New("Bot type error")
8+
ErrParamUserID = errors.New("Param error: UserID")
9+
ErrParamMessageID = errors.New("Param error: Message ID")
10+
ErrParamExceedInputLimit = errors.New("Param error: Exceed input limit")
11+
ErrMessageTypeNotSuppored = errors.New("Message type not supported")
12+
ErrEncryptionNotEnabled = errors.New("Encryption is not enabled")
13+
ErrCustomHTTPClientNotSet = errors.New("Custom HTTP client not set")
14+
ErrMessageNotBuild = errors.New("Message not build")
15+
ErrUnsupportedUIDType = errors.New("Unsupported UID type")
16+
ErrInvalidReceiveID = errors.New("Invalid receive ID")
17+
ErrEventTypeNotMatch = errors.New("Event type not match")
18+
ErrMessageType = errors.New("Message type error")
19+
)

v2/go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/go-lark/lark/v2
2+
3+
go 1.24
4+
5+
require (
6+
github.com/joho/godotenv v1.5.1
7+
github.com/stretchr/testify v1.10.0
8+
)
9+
10+
require (
11+
github.com/davecgh/go-spew v1.1.1 // indirect
12+
github.com/pmezard/go-difflib v1.0.0 // indirect
13+
gopkg.in/yaml.v3 v3.0.1 // indirect
14+
)

v2/go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
2+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
4+
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
5+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
6+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
7+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
8+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
9+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
10+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
11+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
12+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

v2/http.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package lark
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"io"
9+
"net/http"
10+
"net/http/httputil"
11+
)
12+
13+
// ExpandURL expands url path to full url
14+
func (bot Bot) ExpandURL(urlPath string) string {
15+
url := fmt.Sprintf("%s%s", bot.domain, urlPath)
16+
return url
17+
}
18+
19+
func (bot Bot) httpErrorLog(ctx context.Context, prefix, text string, err error) {
20+
bot.logger.Log(ctx, LogLevelError, fmt.Sprintf("[%s] %s: %+v\n", prefix, text, err))
21+
}
22+
23+
// PerformAPIRequest performs API request
24+
func (bot Bot) PerformAPIRequest(
25+
ctx context.Context,
26+
method string,
27+
prefix, urlPath string,
28+
header http.Header, auth bool,
29+
body io.Reader,
30+
output interface{},
31+
) error {
32+
var (
33+
err error
34+
respBody io.ReadCloser
35+
url = bot.ExpandURL(urlPath)
36+
)
37+
if header == nil {
38+
header = make(http.Header)
39+
}
40+
if auth {
41+
header.Add("Authorization", fmt.Sprintf("Bearer %s", bot.TenantAccessToken()))
42+
}
43+
if bot.useCustomClient {
44+
if bot.customClient == nil {
45+
return ErrCustomHTTPClientNotSet
46+
}
47+
respBody, err = bot.customClient.Do(ctx, method, url, header, body)
48+
if err != nil {
49+
bot.httpErrorLog(ctx, prefix, "call failed", err)
50+
return err
51+
}
52+
} else {
53+
req, err := http.NewRequestWithContext(ctx, method, url, body)
54+
if err != nil {
55+
bot.httpErrorLog(ctx, prefix, "init request failed", err)
56+
return err
57+
}
58+
req.Header = header
59+
resp, err := bot.client.Do(req)
60+
if err != nil {
61+
bot.httpErrorLog(ctx, prefix, "call failed", err)
62+
return err
63+
}
64+
if bot.debug {
65+
b, _ := httputil.DumpResponse(resp, true)
66+
bot.logger.Log(ctx, LogLevelDebug, string(b))
67+
}
68+
respBody = resp.Body
69+
}
70+
defer respBody.Close()
71+
err = json.NewDecoder(respBody).Decode(&output)
72+
if err != nil {
73+
bot.httpErrorLog(ctx, prefix, "decode body failed", err)
74+
return err
75+
}
76+
return err
77+
}
78+
79+
func (bot Bot) wrapAPIRequest(ctx context.Context, method, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
80+
buf := new(bytes.Buffer)
81+
err := json.NewEncoder(buf).Encode(params)
82+
if err != nil {
83+
bot.httpErrorLog(ctx, prefix, "encode JSON failed", err)
84+
return err
85+
}
86+
87+
header := make(http.Header)
88+
header.Set("Content-Type", "application/json; charset=utf-8")
89+
err = bot.PerformAPIRequest(ctx, method, prefix, urlPath, header, auth, buf, output)
90+
if err != nil {
91+
return err
92+
}
93+
return nil
94+
}
95+
96+
// PostAPIRequest call Lark API
97+
func (bot Bot) PostAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
98+
return bot.wrapAPIRequest(ctx, http.MethodPost, prefix, urlPath, auth, params, output)
99+
}
100+
101+
// GetAPIRequest call Lark API
102+
func (bot Bot) GetAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
103+
return bot.wrapAPIRequest(ctx, http.MethodGet, prefix, urlPath, auth, params, output)
104+
}
105+
106+
// DeleteAPIRequest call Lark API
107+
func (bot Bot) DeleteAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
108+
return bot.wrapAPIRequest(ctx, http.MethodDelete, prefix, urlPath, auth, params, output)
109+
}
110+
111+
// PutAPIRequest call Lark API
112+
func (bot Bot) PutAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
113+
return bot.wrapAPIRequest(ctx, http.MethodPut, prefix, urlPath, auth, params, output)
114+
}
115+
116+
// PatchAPIRequest call Lark API
117+
func (bot Bot) PatchAPIRequest(ctx context.Context, prefix, urlPath string, auth bool, params interface{}, output interface{}) error {
118+
return bot.wrapAPIRequest(ctx, http.MethodPatch, prefix, urlPath, auth, params, output)
119+
}

v2/http_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package lark
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestExpandURL(t *testing.T) {
10+
bot := NewChatBot("test-id", "test-secret")
11+
bot.SetDomain("http://localhost")
12+
assert.Equal(t, bot.ExpandURL("/test"),
13+
"http://localhost/test")
14+
}

v2/http_wrapper.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package lark
2+
3+
import (
4+
"context"
5+
"io"
6+
"net/http"
7+
)
8+
9+
// HTTPWrapper is a wrapper interface, which enables extension on HTTP part.
10+
// Typicall, we do not need this because default client is sufficient.
11+
type HTTPWrapper interface {
12+
Do(
13+
ctx context.Context,
14+
method, url string,
15+
header http.Header,
16+
body io.Reader) (io.ReadCloser, error)
17+
}

v2/lark.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
// Package lark is an easy-to-use SDK for Feishu and Lark Open Platform,
2+
// which implements messaging APIs, with full-fledged supports on building Chat Bot and Notification Bot.
3+
package lark
4+
5+
import (
6+
"context"
7+
"net/http"
8+
"sync/atomic"
9+
"time"
10+
)
11+
12+
const (
13+
// ChatBot should be created with NewChatBot
14+
// Create from https://open.feishu.cn/ or https://open.larksuite.com/
15+
ChatBot = iota
16+
// NotificationBot for webhook, behave as a simpler notification bot
17+
// Create from Lark group
18+
NotificationBot
19+
)
20+
21+
// Bot definition
22+
type Bot struct {
23+
// bot type
24+
botType int
25+
// Auth info
26+
appID string
27+
appSecret string
28+
accessToken atomic.Value
29+
tenantAccessToken atomic.Value
30+
31+
// user id type for api chat
32+
userIDType string
33+
// webhook for NotificationBot
34+
webhook string
35+
// API Domain
36+
domain string
37+
// http client
38+
client *http.Client
39+
// custom http client
40+
useCustomClient bool
41+
customClient HTTPWrapper
42+
// auth heartbeat
43+
heartbeat chan bool
44+
// auth heartbeat context
45+
heartbeatCtx context.Context
46+
// auth heartbeat counter (for testing)
47+
heartbeatCounter int64
48+
49+
logger LogWrapper
50+
debug bool
51+
}
52+
53+
// Domains
54+
const (
55+
DomainFeishu = "https://open.feishu.cn"
56+
DomainLark = "https://open.larksuite.com"
57+
)
58+
59+
// NewChatBot with appID and appSecret
60+
func NewChatBot(appID, appSecret string) *Bot {
61+
bot := &Bot{
62+
botType: ChatBot,
63+
appID: appID,
64+
appSecret: appSecret,
65+
client: initClient(),
66+
domain: DomainFeishu,
67+
heartbeatCtx: context.Background(),
68+
logger: initDefaultLogger(),
69+
}
70+
bot.accessToken.Store("")
71+
bot.tenantAccessToken.Store("")
72+
73+
return bot
74+
}
75+
76+
// NewNotificationBot with URL
77+
func NewNotificationBot(hookURL string) *Bot {
78+
bot := &Bot{
79+
botType: NotificationBot,
80+
webhook: hookURL,
81+
client: initClient(),
82+
logger: initDefaultLogger(),
83+
}
84+
bot.accessToken.Store("")
85+
bot.tenantAccessToken.Store("")
86+
87+
return bot
88+
}
89+
90+
// requireType checks whether the action is allowed in a list of bot types
91+
func (bot Bot) requireType(botType ...int) bool {
92+
for _, iterType := range botType {
93+
if bot.botType == iterType {
94+
return true
95+
}
96+
}
97+
return false
98+
}
99+
100+
// SetClient assigns a new client to bot.client
101+
func (bot *Bot) SetClient(c *http.Client) {
102+
bot.client = c
103+
}
104+
105+
func initClient() *http.Client {
106+
return &http.Client{
107+
Timeout: 5 * time.Second,
108+
}
109+
}
110+
111+
// SetCustomClient .
112+
func (bot *Bot) SetCustomClient(c HTTPWrapper) {
113+
bot.useCustomClient = true
114+
bot.customClient = c
115+
}
116+
117+
// UnsetCustomClient .
118+
func (bot *Bot) UnsetCustomClient() {
119+
bot.useCustomClient = false
120+
bot.customClient = nil
121+
}
122+
123+
// SetDomain set domain of endpoint, so we could call Feishu/Lark
124+
// go-lark does not check your host, just use the right one or fail.
125+
func (bot *Bot) SetDomain(domain string) {
126+
bot.domain = domain
127+
}
128+
129+
// Domain returns current domain
130+
func (bot Bot) Domain() string {
131+
return bot.domain
132+
}
133+
134+
// AppID returns bot.appID for external use
135+
func (bot Bot) AppID() string {
136+
return bot.appID
137+
}
138+
139+
// BotType returns bot.botType for external use
140+
func (bot Bot) BotType() int {
141+
return bot.botType
142+
}
143+
144+
// AccessToken returns bot.accessToken for external use
145+
func (bot Bot) AccessToken() string {
146+
return bot.accessToken.Load().(string)
147+
}
148+
149+
// TenantAccessToken returns bot.tenantAccessToken for external use
150+
func (bot Bot) TenantAccessToken() string {
151+
return bot.tenantAccessToken.Load().(string)
152+
}
153+
154+
// SetWebhook updates webhook URL
155+
func (bot *Bot) SetWebhook(url string) {
156+
bot.webhook = url
157+
}

0 commit comments

Comments
 (0)