From a6e4794e155cb8f7434414d86952451b8c518573 Mon Sep 17 00:00:00 2001 From: joyqi Date: Wed, 21 Sep 2022 15:51:58 +0800 Subject: [PATCH] Release/v0.2.0 (#3) * Feature/user api (#1) * Init user api * Add first method * Add ClientTokenSource interface as an abstract layer of api token provider * Add request interface * Fix readme * fix token url * fix oauth * fix interface * fix tokensource * add authen api * fix test * fix test * fix time * fix test * fix app * fix token * add Type * add lark supports * fix readme * change feishu -> lafi --- .github/workflows/test.yml | 2 + README.md | 41 +++++++---- api/api.go | 3 + api/auth/app.go | 33 +++++++++ api/auth/auth.go | 51 ++++++++++++++ api/auth/tenant.go | 32 +++++++++ api/authen/accesstoken.go | 50 ++++++++++++++ api/authen/userinfo.go | 34 +++++++++ api/contact/group.go | 20 +++--- api/contact/group_test.go | 9 ++- api/contact/user.go | 6 +- go.mod | 2 +- httptool/client_test.go | 4 +- oauth2/client.go | 53 ++++++++++---- oauth2/oauth2.go | 91 +++++++++++------------- oauth2/oauth2_test.go | 39 ++++++++++- oauth2/tenant.go | 138 ++++++++++++++++++++++++++----------- oauth2/token.go | 39 ++--------- 18 files changed, 477 insertions(+), 170 deletions(-) create mode 100644 api/auth/app.go create mode 100644 api/auth/auth.go create mode 100644 api/auth/tenant.go create mode 100644 api/authen/accesstoken.go create mode 100644 api/authen/userinfo.go diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c3af25c..724515d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,4 +24,6 @@ jobs: env: APP_ID: '${{ secrets.FEISHU_APP_ID }}' APP_SECRET: '${{ secrets.FEISHU_APP_SECRET }}' + ACCESS_TOKEN: '${{ secrets.FEISHU_ACCESS_TOKEN }}' + REFRESH_TOKEN: '${{ secrets.FEISHU_REFRESH_TOKEN }}' run: go test -v ./... \ No newline at end of file diff --git a/README.md b/README.md index 5223fbc..76b3adb 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,12 @@ -# go-feishu +# go-lafi -go-feishu is a Go client library for accessing the Feishu API. +**lafi = lark + feishu** -> As of now I am writing this library, there is no official Feishu SDK for Go. +go-lafi is a Go client library for accessing the Lark/Feishu API. + +> As of now I am writing this library, there is no official Lark/Feishu SDK for Go. > Although Bytedance is the fastest growing tech company in China, -> seems like they don't want to hire a Go developer to write a Go SDK for Feishu. 😂 +> seems like they don't want to hire a Go developer to write a Go SDK for Lark/Feishu. 😂 The goals of this library are: @@ -15,22 +17,35 @@ The goals of this library are: ## Installation ```bash -go get github.com/joyqi/go-feishu +go get github.com/joyqi/go-lafi ``` ## Usage ### OAuth2 Authentication -Initialize Feishu client with OAuth2 authentication: +Initialize Lark client with OAuth2 authentication: + +```go +import "github.com/joyqi/go-lafi/oauth2" + +var conf = &oauth2.Config{ + AppID: "your-client-id", + AppSecret: "your-client-secret", + RedirectURL: "your-redirect-url", +} +``` + +For Feishu, you can specify the `Type` field to `TypeFeishu`: ```go -import "github.com/joyqi/go-feishu/oauth2" +import "github.com/joyqi/go-lafi/oauth2" var conf = &oauth2.Config{ AppID: "your-client-id", AppSecret: "your-client-secret", RedirectURL: "your-redirect-url", + Type: oauth2.TypeFeishu, } ``` @@ -53,7 +68,7 @@ ts := conf.TokenSource(ctx, token) token, err := ts.Token() ``` -### Feishu API +### Lark/Feishu API Features: @@ -66,20 +81,20 @@ Features: - [ ] CustomAttr - [ ] Scope -Initialize Feishu API client: +Initialize API client: ```go -import "github.com/joyqi/go-feishu/oauth2" +import "github.com/joyqi/go-lafi/oauth2" client := conf.TenantTokenSource(ctx).Client() ``` -Use the client to access the Feishu API. For example, to list all groups: +Use the client to access the API. For example, to list all groups: ```go import ( - "github.com/joyqi/go-feishu/oauth2" - "github.com/joyqi/go-feishu/api/contact" + "github.com/joyqi/go-lafi/oauth2" + "github.com/joyqi/go-lafi/api/contact" ) client := conf.TenantTokenSource(ctx).Client() diff --git a/api/api.go b/api/api.go index 99d468e..6564eae 100644 --- a/api/api.go +++ b/api/api.go @@ -8,6 +8,9 @@ type Api struct { Client } +type EmptyData struct { +} + // Client defines the interface of api client type Client interface { Request(method string, uri string, body interface{}, data interface{}) error diff --git a/api/auth/app.go b/api/auth/app.go new file mode 100644 index 0000000..1da5997 --- /dev/null +++ b/api/auth/app.go @@ -0,0 +1,33 @@ +package auth + +import "github.com/joyqi/go-lafi/api" + +const ( + AppCommonURL = "/auth/v3/app_access_token" + AppInternalURL = "/auth/v3/app_access_token/internal" +) + +// AppCommonBody represents a request to retrieve an app token +type AppCommonBody struct { + AppID string `json:"app_id"` + AppSecret string `json:"app_secret"` + AppTicket string `json:"app_ticket"` +} + +// AppInternalBody represents a request to retrieve an app token +type AppInternalBody struct { + AppID string `json:"app_id"` + AppSecret string `json:"app_secret"` +} + +type App api.Api + +// CommonAccessToken retrieves a common token from the app token endpoint +func (a *App) CommonAccessToken(body *AppCommonBody) (string, int64, error) { + return MakeTokenApi(a, "app_access_token", AppCommonURL, body) +} + +// InternalAccessToken retrieves an internal token from the app token endpoint +func (a *App) InternalAccessToken(body *AppInternalBody) (string, int64, error) { + return MakeTokenApi(a, "app_access_token", AppInternalURL, body) +} diff --git a/api/auth/auth.go b/api/auth/auth.go new file mode 100644 index 0000000..95fb540 --- /dev/null +++ b/api/auth/auth.go @@ -0,0 +1,51 @@ +package auth + +import ( + "encoding/json" + "errors" + "github.com/joyqi/go-lafi/api" +) + +// TokenResponse represents the common token response structure +type TokenResponse struct { + // Code is the response status code + Code int `json:"code"` + + // Msg is the response message + Msg string `json:"msg"` + + // AccessToken is the access token + AccessToken string + + // Expire is the expiration time of the access token + Expire int64 `json:"expire"` +} + +// MakeTokenApi creates a new token api +func MakeTokenApi(c api.Client, tokenName string, uri string, body interface{}) (string, int64, error) { + var resp map[string]json.RawMessage + token := TokenResponse{} + err := c.Request("POST", uri, body, &resp) + + if err = json.Unmarshal(resp["code"], &token.Code); err != nil { + return "", 0, err + } + + if err = json.Unmarshal(resp["msg"], &token.Msg); err != nil { + return "", 0, err + } + + if err = json.Unmarshal(resp[tokenName], &token.AccessToken); err != nil { + return "", 0, err + } + + if err = json.Unmarshal(resp["expire"], &token.Expire); err != nil { + return "", 0, err + } + + if token.Code != 0 { + return "", 0, errors.New(token.Msg) + } + + return token.AccessToken, token.Expire, nil +} diff --git a/api/auth/tenant.go b/api/auth/tenant.go new file mode 100644 index 0000000..1179cf6 --- /dev/null +++ b/api/auth/tenant.go @@ -0,0 +1,32 @@ +package auth + +import "github.com/joyqi/go-lafi/api" + +const ( + TenantCommonURL = "/auth/v3/tenant_access_token" + TenantInternalURL = "/auth/v3/tenant_access_token/internal" +) + +// TenantCommonBody represents a request to retrieve a tenant token +type TenantCommonBody struct { + AppAccessToken string `json:"app_access_token"` + TenantKey string `json:"tenant_key"` +} + +// TenantInternalBody represents a request to retrieve a tenant token +type TenantInternalBody struct { + AppID string `json:"app_id"` + AppSecret string `json:"app_secret"` +} + +type Tenant api.Api + +// CommonAccessToken retrieves a common token from the tenant token endpoint +func (t *Tenant) CommonAccessToken(body *TenantCommonBody) (string, int64, error) { + return MakeTokenApi(t, "tenant_access_token", TenantCommonURL, body) +} + +// InternalAccessToken retrieves a internal token from the tenant token endpoint +func (t *Tenant) InternalAccessToken(body *TenantInternalBody) (string, int64, error) { + return MakeTokenApi(t, "tenant_access_token", TenantInternalURL, body) +} diff --git a/api/authen/accesstoken.go b/api/authen/accesstoken.go new file mode 100644 index 0000000..d47e94f --- /dev/null +++ b/api/authen/accesstoken.go @@ -0,0 +1,50 @@ +package authen + +import ( + "github.com/joyqi/go-lafi/api" + "net/http" +) + +const ( + AccessTokenURL = "/authen/v1/access_token" + AccessTokenRefreshURL = "/authen/v1/refresh_access_token" +) + +// AccessTokenCreateBody represents the request body of creating AccessToken +type AccessTokenCreateBody struct { + GrantType string `json:"grant_type"` + Code string `json:"code"` +} + +// AccessTokenData represents the data of creating AccessToken +type AccessTokenData struct { + // OpenId represents the open ID of the user + OpenId string `json:"open_id"` + + // AccessToken is the token used to access the application + AccessToken string `json:"access_token"` + + // RefreshToken is the token used to refresh the user's access token + RefreshToken string `json:"refresh_token"` + + // ExpiresIn is the number of seconds the token will be valid + ExpiresIn int64 `json:"expires_in"` +} + +// AccessTokenRefreshBody represents the request body of refreshing AccessToken +type AccessTokenRefreshBody struct { + GrantType string `json:"grant_type"` + RefreshToken string `json:"refresh_token"` +} + +type AccessToken api.Api + +// Create creates the access token. +func (a *AccessToken) Create(body *AccessTokenCreateBody) (*AccessTokenData, error) { + return api.MakeApi[AccessTokenData](a.Client, http.MethodPost, AccessTokenURL, body) +} + +// Refresh refreshes the access token. +func (a *AccessToken) Refresh(body *AccessTokenRefreshBody) (*AccessTokenData, error) { + return api.MakeApi[AccessTokenData](a.Client, http.MethodPost, AccessTokenRefreshURL, body) +} diff --git a/api/authen/userinfo.go b/api/authen/userinfo.go new file mode 100644 index 0000000..1b19d3f --- /dev/null +++ b/api/authen/userinfo.go @@ -0,0 +1,34 @@ +package authen + +import ( + "github.com/joyqi/go-lafi/api" + "net/http" +) + +const ( + UserInfoURL = "/authen/v1/user_info" +) + +// UserInfoData represents the response data of UserInfo +type UserInfoData struct { + Name string `json:"name"` + EnName string `json:"en_name"` + AvatarURL string `json:"avatar_url"` + AvatarThumb string `json:"avatar_thumb"` + AvatarMiddle string `json:"avatar_middle"` + AvatarBig string `json:"avatar_big"` + OpenId string `json:"open_id"` + UnionId string `json:"union_id"` + Email string `json:"email"` + EnterpriseEmail string `json:"enterprise_email"` + UserId string `json:"user_id"` + Mobile string `json:"mobile"` + TenantKey string `json:"tenant_key"` +} + +type UserInfo api.Api + +// Get fetches the user info through the access token. +func (a *UserInfo) Get() (data *UserInfoData, err error) { + return api.MakeApi[UserInfoData](a.Client, http.MethodGet, UserInfoURL, nil) +} diff --git a/api/contact/group.go b/api/contact/group.go index 5ff5ad7..546ae0e 100644 --- a/api/contact/group.go +++ b/api/contact/group.go @@ -2,16 +2,16 @@ package contact import ( "github.com/creasty/defaults" - "github.com/joyqi/go-feishu/api" - "github.com/joyqi/go-feishu/httptool" + "github.com/joyqi/go-lafi/api" + "github.com/joyqi/go-lafi/httptool" "net/http" ) const ( - GroupURL = "https://open.feishu.cn/open-apis/contact/v3/group/:group_id" - GroupCreateURL = "https://open.feishu.cn/open-apis/contact/v3/group" - GroupSimpleListURL = "https://open.feishu.cn/open-apis/contact/v3/group/simplelist" - GroupMemberBelongURL = "https://open.feishu.cn/open-apis/contact/v3/group/member_belong" + GroupURL = "/contact/v3/group/:group_id" + GroupCreateURL = "/contact/v3/group" + GroupSimpleListURL = "/contact/v3/group/simplelist" + GroupMemberBelongURL = "/contact/v3/group/member_belong" ) // GroupType represents the type of group. @@ -44,12 +44,10 @@ type GroupPatchBody struct { } // GroupPatchData represents the response data of Group.Patch -type GroupPatchData struct { -} +type GroupPatchData = api.EmptyData // GroupDeleteData represents the response data of Group.Delete -type GroupDeleteData struct { -} +type GroupDeleteData = api.EmptyData // GroupGetData represents the response data of Group.Get type GroupGetData struct { @@ -94,7 +92,7 @@ type GroupMemberBelongParams struct { // GroupMemberBelongData represents the response data of Group.MemberBelong type GroupMemberBelongData struct { - GroupList []string `json:"group_list,flow"` + GroupList []string `json:"group_list"` HasMore bool `json:"has_more"` PageToken string `json:"page_token"` } diff --git a/api/contact/group_test.go b/api/contact/group_test.go index 19ad28d..e34a633 100644 --- a/api/contact/group_test.go +++ b/api/contact/group_test.go @@ -2,7 +2,7 @@ package contact import ( "context" - "github.com/joyqi/go-feishu/oauth2" + "github.com/joyqi/go-lafi/oauth2" "os" "testing" ) @@ -11,6 +11,7 @@ var conf = &oauth2.Config{ AppID: os.Getenv("APP_ID"), AppSecret: os.Getenv("APP_SECRET"), RedirectURL: "https://example.com", + Type: oauth2.TypeFeishu, } var client = conf.TenantTokenSource(context.Background()).Client() @@ -25,9 +26,7 @@ func TestGroup_Create(t *testing.T) { if err != nil { t.Error(err) - } - - if data.GroupId != "test001" { + } else if data.GroupId != "test001" { t.Fail() } } @@ -52,7 +51,7 @@ func TestGroup_Patch(t *testing.T) { }) if err != nil { - t.Error(err) + t.Fatal(err) } g, err := a.Get("test001") diff --git a/api/contact/user.go b/api/contact/user.go index 2bde99a..74808e5 100644 --- a/api/contact/user.go +++ b/api/contact/user.go @@ -2,13 +2,13 @@ package contact import ( "github.com/creasty/defaults" - "github.com/joyqi/go-feishu/api" - "github.com/joyqi/go-feishu/httptool" + "github.com/joyqi/go-lafi/api" + "github.com/joyqi/go-lafi/httptool" "net/http" ) const ( - UserCreateURL = "https://open.feishu.cn/open-apis/contact/v3/users" + UserCreateURL = "/contact/v3/users" ) const ( diff --git a/go.mod b/go.mod index 121eb09..f3d508b 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/joyqi/go-feishu +module github.com/joyqi/go-lafi go 1.18 diff --git a/httptool/client_test.go b/httptool/client_test.go index 1961aa9..5cc9321 100644 --- a/httptool/client_test.go +++ b/httptool/client_test.go @@ -57,9 +57,7 @@ func TestRequest_JSONResponse(t *testing.T) { if err != nil { t.Error(err) - } - - if !reflect.DeepEqual(req, resp.JSON) { + } else if !reflect.DeepEqual(req, resp.JSON) { t.Fail() } } diff --git a/oauth2/client.go b/oauth2/client.go index b25f32a..0934663 100644 --- a/oauth2/client.go +++ b/oauth2/client.go @@ -2,14 +2,37 @@ package oauth2 import ( "context" - "github.com/joyqi/go-feishu/httptool" - "net/http" + "github.com/joyqi/go-lafi/api" + "github.com/joyqi/go-lafi/httptool" ) -// A Client represents a http client with an authorized token. +// ClientSource represents the source which provides a Client method to retrieve a Client. +type ClientSource interface { + TokenSource + Client() api.Client +} + +// simpleClient is a client that does not use a token. +type simpleClient struct { + ctx context.Context + t Type +} + +// Request performs a http request to the given endpoint without the authorized token. +func (c *simpleClient) Request(method string, uri string, body interface{}, data interface{}) error { + return httptool.Request(c.ctx, &httptool.RequestOptions{ + URI: adjustURL(c.t, uri), + Method: method, + ContentType: "application/json; charset=utf-8", + JSONBody: body, + }, data) +} + +// tokenClient represents a client that performs requests with an authorized token. type tokenClient struct { ctx context.Context ts TokenSource + t Type } // Request performs a http request to the given endpoint with the authorized token. @@ -25,7 +48,7 @@ func (c *tokenClient) Request(method string, uri string, body interface{}, data } return httptool.Request(c.ctx, &httptool.RequestOptions{ - URI: uri, + URI: adjustURL(c.t, uri), Method: method, Headers: []httptool.Header{header}, ContentType: "application/json; charset=utf-8", @@ -33,12 +56,18 @@ func (c *tokenClient) Request(method string, uri string, body interface{}, data }, data) } -func httpPost(ctx context.Context, uri string, body interface{}, data interface{}, headers ...httptool.Header) error { - return httptool.Request(ctx, &httptool.RequestOptions{ - URI: uri, - Method: http.MethodPost, - Headers: headers, - ContentType: "application/json; charset=utf-8", - JSONBody: body, - }, data) +// adjustURL adjusts the url based on the type of the client. +// For TypeFeishu, the url will be prefixed with the Feishu API URL. +// For TypeLark, the url will be prefixed with the Lark API URL. +func adjustURL(t Type, url string) string { + prefix := "" + + switch t { + case TypeFeishu: + prefix = "https://open.feishu.cn/open-apis" + case TypeLark: + prefix = "https://open.larksuite.com/open-apis" + } + + return prefix + url } diff --git a/oauth2/oauth2.go b/oauth2/oauth2.go index 63c41e5..9ae7946 100644 --- a/oauth2/oauth2.go +++ b/oauth2/oauth2.go @@ -2,16 +2,20 @@ package oauth2 import ( "context" - "github.com/joyqi/go-feishu/api" - "github.com/joyqi/go-feishu/httptool" + "github.com/joyqi/go-lafi/api" + "github.com/joyqi/go-lafi/api/authen" + "github.com/joyqi/go-lafi/httptool" "net/url" "sync" ) +const AuthURL = "/authen/v1/index" + +type Type int8 + const ( - AuthURL = "https://open.feishu.cn/open-apis/authen/v1/index" - TokenURL = "https://open.feishu.cn/open-apis/authen/v1/access_token" - RefreshTokenURL = "https://open.feishu.cn/open-apis/authen/v1/refresh_access_token" + TypeLark Type = iota + TypeFeishu ) // Config represents the configuration of the oauth2 service @@ -22,56 +26,30 @@ type Config struct { // AppSecret is the app secret of oauth2. AppSecret string + // AppTicket represents the ticket of the app. + // It's used to retrieve the tenant access token. + // If you're using the internal app, please leave it empty. + AppTicket string + + // TenantKey represents the key of the tenant. + TenantKey string + // RedirectURL is the URL to redirect users going through RedirectURL string + // Type represents the type of the app. + Type + // once is used to ensure tts is initialized only once. once sync.Once // tts is the tenant token source - tts TokenSource + tts ClientSource } // A TokenSource is anything that can return a token. type TokenSource interface { Token() (*Token, error) - Client() api.Client -} - -// TokenRequest represents a request to retrieve a token from the server -type TokenRequest struct { - GrantType string `json:"grant_type"` - Code string `json:"code"` -} - -// RefreshTokenRequest represents a request to refresh the token from the server -type RefreshTokenRequest struct { - GrantType string `json:"grant_type"` - RefreshToken string `json:"refresh_token"` -} - -// TokenResponse represents the response from the Token service -type TokenResponse struct { - // Code is the response status code - Code int `json:"code"` - - // Msg is the response message in the response body - Msg string `json:"msg"` - - // Data is the response body data - Data struct { - // OpenId represents the open ID of the user - OpenId string `json:"open_id"` - - // AccessToken is the token used to access the application - AccessToken string `json:"access_token"` - - // RefreshToken is the token used to refresh the user's access token - RefreshToken string `json:"refresh_token"` - - // ExpiresIn is the number of seconds the token will be valid - ExpiresIn int64 `json:"expires_in"` - } `json:"data"` } // AuthCodeURL is the URL to redirect users going through authentication @@ -88,21 +66,28 @@ func (c *Config) AuthCodeURL(state string) string { v.Set("state", state) } - return httptool.MakeURL(AuthURL, v) + return httptool.MakeURL(adjustURL(c.Type, AuthURL), v) } // Exchange retrieve the token from access token endpoint func (c *Config) Exchange(ctx context.Context, code string) (*Token, error) { - req := &TokenRequest{ + req := &authen.AccessTokenCreateBody{ GrantType: "authorization_code", Code: code, } - return retrieveToken(ctx, TokenURL, req, c.TenantTokenSource(ctx)) + tokenApi := &authen.AccessToken{Client: c.TenantTokenSource(ctx).Client()} + tk, err := tokenApi.Create(req) + + if err != nil { + return nil, err + } + + return NewToken(tk), nil } // TokenSource returns a TokenSource to grant token access -func (c *Config) TokenSource(ctx context.Context, t *Token) TokenSource { +func (c *Config) TokenSource(ctx context.Context, t *Token) ClientSource { return &reuseTokenSource{ ctx: ctx, conf: c, @@ -136,15 +121,23 @@ func (s *reuseTokenSource) Client() api.Client { return &tokenClient{ ctx: s.ctx, ts: s, + t: s.conf.Type, } } // refresh retrieves the token from the endpoint func (s *reuseTokenSource) refresh() (*Token, error) { - req := &RefreshTokenRequest{ + req := &authen.AccessTokenRefreshBody{ RefreshToken: s.t.RefreshToken, GrantType: "refresh_token", } - return retrieveToken(s.ctx, RefreshTokenURL, req, s.conf.TenantTokenSource(s.ctx)) + tokenApi := &authen.AccessToken{Client: s.conf.TenantTokenSource(s.ctx).Client()} + tk, err := tokenApi.Refresh(req) + + if err != nil { + return nil, err + } + + return NewToken(tk), nil } diff --git a/oauth2/oauth2_test.go b/oauth2/oauth2_test.go index 046f9cc..fe36291 100644 --- a/oauth2/oauth2_test.go +++ b/oauth2/oauth2_test.go @@ -10,10 +10,11 @@ var conf = &Config{ AppID: os.Getenv("APP_ID"), AppSecret: os.Getenv("APP_SECRET"), RedirectURL: "https://example.com", + Type: TypeFeishu, } func TestConfig_AuthCodeURL(t *testing.T) { - target := AuthURL + "?app_id=" + conf.AppID + + target := adjustURL(TypeFeishu, AuthURL) + "?app_id=" + conf.AppID + "&redirect_uri=https%3A%2F%2Fexample.com&response_type=code&state=test" if conf.AuthCodeURL("test") != target { t.Fail() @@ -28,3 +29,39 @@ func TestConfig_TenantToken(t *testing.T) { t.Error(err) } } + +/* +func TestReuseTokenSource_Token(t *testing.T) { + var tk *Token + ch := make(chan *Token, 1) + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + tk, err := conf.Exchange(context.Background(), r.URL.Query().Get("code")) + + if err != nil { + t.Error(err) + } else { + ch <- tk + } + })) + + defer server.Close() + fmt.Println(server.URL) + + select { + case tk = <-ch: + if tk == nil { + t.Fail() + } + } + + ts := conf.TokenSource(context.Background(), tk) + nt, err := ts.Token() + + if err != nil { + t.Error(err) + } else if tk.AccessToken == nt.AccessToken { + t.Fail() + } +} +*/ diff --git a/oauth2/tenant.go b/oauth2/tenant.go index 9ea19f5..58195ca 100644 --- a/oauth2/tenant.go +++ b/oauth2/tenant.go @@ -2,41 +2,28 @@ package oauth2 import ( "context" - "errors" - "github.com/joyqi/go-feishu/api" + "github.com/joyqi/go-lafi/api" + "github.com/joyqi/go-lafi/api/auth" "sync" "time" ) -const TenantTokenURL = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" - -// TenantTokenRequest represents a request to retrieve a tenant token -type TenantTokenRequest struct { - AppID string `json:"app_id"` - AppSecret string `json:"app_secret"` -} - -// TenantTokenResponse is the token response of the tenant endpoint -type TenantTokenResponse struct { - // Code is the response status code - Code int `json:"code"` - - // Msg is the response message - Msg string `json:"msg"` - - // TenantAccessToken is the access token - TenantAccessToken string `json:"tenant_access_token"` - - // Expire is the expiration time of the access token - Expire int64 `json:"expire"` -} - // TenantTokenSource returns a token source that retrieves tokens from the tenant token endpoint -func (c *Config) TenantTokenSource(ctx context.Context) TokenSource { +func (c *Config) TenantTokenSource(ctx context.Context) ClientSource { c.once.Do(func() { + var ats *appTokenSource + + if c.AppTicket != "" && c.TenantKey != "" { + ats = &appTokenSource{ + ctx: ctx, + conf: c, + } + } + c.tts = &tenantTokenSource{ ctx: ctx, conf: c, + ats: ats, } }) @@ -48,6 +35,7 @@ type tenantTokenSource struct { ctx context.Context conf *Config t *Token + ats *appTokenSource mu sync.Mutex } @@ -57,25 +45,22 @@ func (s *tenantTokenSource) Token() (*Token, error) { s.mu.Lock() if s.t == nil || !s.t.Valid() { - req := &TenantTokenRequest{ - AppID: s.conf.AppID, - AppSecret: s.conf.AppSecret, + var ( + err error + t *Token + ) + + if s.ats != nil { + t, err = s.retrieveCommonToken() + } else { + t, err = s.retrieveInternalToken() } - resp := TenantTokenResponse{} - err := httpPost(s.ctx, TenantTokenURL, req, &resp) if err != nil { return nil, err } - if resp.Code != 0 { - return nil, errors.New(resp.Msg) - } - - s.t = &Token{ - AccessToken: resp.TenantAccessToken, - Expiry: time.Now().Add(time.Duration(resp.Expire) * time.Second), - } + s.t = t } return s.t, nil @@ -86,5 +71,80 @@ func (s *tenantTokenSource) Client() api.Client { return &tokenClient{ ctx: s.ctx, ts: s, + t: s.conf.Type, + } +} + +func (s *tenantTokenSource) retrieveInternalToken() (*Token, error) { + c := &simpleClient{ctx: s.ctx} + authApi := &auth.Tenant{Client: c} + + tk, expire, err := authApi.InternalAccessToken(&auth.TenantInternalBody{ + AppID: s.conf.AppID, + AppSecret: s.conf.AppSecret, + }) + + if err != nil { + return nil, err + } + + return &Token{ + AccessToken: tk, + Expiry: time.Now().Add(time.Duration(expire) * time.Second), + }, nil +} + +func (s *tenantTokenSource) retrieveCommonToken() (*Token, error) { + t, err := s.ats.Token() + if err != nil { + return nil, err } + + c := &simpleClient{ctx: s.ctx} + authApi := &auth.Tenant{Client: c} + + tk, expire, err := authApi.CommonAccessToken(&auth.TenantCommonBody{ + AppAccessToken: t.AccessToken, + TenantKey: s.conf.TenantKey, + }) + + if err != nil { + return nil, err + } + + return &Token{ + AccessToken: tk, + Expiry: time.Now().Add(time.Duration(expire) * time.Second), + }, nil +} + +type appTokenSource struct { + ctx context.Context + conf *Config + t *Token +} + +// Token retrieves a token from the app token endpoint +func (s *appTokenSource) Token() (*Token, error) { + if s.t == nil || !s.t.Valid() { + c := &simpleClient{ctx: s.ctx} + authApi := &auth.App{Client: c} + + tk, expire, err := authApi.CommonAccessToken(&auth.AppCommonBody{ + AppID: s.conf.AppID, + AppSecret: s.conf.AppSecret, + AppTicket: s.conf.AppTicket, + }) + + if err != nil { + return nil, err + } + + s.t = &Token{ + AccessToken: tk, + Expiry: time.Now().Add(time.Duration(expire) * time.Second), + } + } + + return s.t, nil } diff --git a/oauth2/token.go b/oauth2/token.go index e053929..6986ac0 100644 --- a/oauth2/token.go +++ b/oauth2/token.go @@ -1,9 +1,7 @@ package oauth2 import ( - "context" - "errors" - "github.com/joyqi/go-feishu/httptool" + "github.com/joyqi/go-lafi/api/authen" "time" ) @@ -18,35 +16,10 @@ func (t *Token) Valid() bool { return time.Now().Add(time.Minute).Before(t.Expiry) } -// retrieveToken retrieves the token from the endpoint -func retrieveToken(ctx context.Context, endpointURL string, req interface{}, ts TokenSource) (*Token, error) { - t, err := ts.Token() - if err != nil { - return nil, err +func NewToken(tk *authen.AccessTokenData) *Token { + return &Token{ + AccessToken: tk.AccessToken, + RefreshToken: tk.RefreshToken, + Expiry: time.Now().Add(time.Duration(tk.ExpiresIn) * time.Second), } - - resp := TokenResponse{} - err = httpPost( - ctx, - endpointURL, - req, - &resp, - httptool.Header{Key: "Authorization", Value: t.AccessToken}, - ) - - if err != nil { - return nil, err - } - - if resp.Code != 0 { - return nil, errors.New(resp.Msg) - } - - token := &Token{ - AccessToken: resp.Data.AccessToken, - RefreshToken: resp.Data.RefreshToken, - Expiry: time.Unix(resp.Data.ExpiresIn, 0), - } - - return token, nil }