348 lines
10 KiB
Go
348 lines
10 KiB
Go
package wecom
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"time"
|
|
|
|
"golang.org/x/oauth2"
|
|
|
|
"github.com/chaitin/panda-wiki/log"
|
|
"github.com/chaitin/panda-wiki/store/cache"
|
|
)
|
|
|
|
const (
|
|
// AuthURL api doc https://developer.work.weixin.qq.com/document/path/98152
|
|
AuthWebURL = "https://login.work.weixin.qq.com/wwlogin/sso/login"
|
|
AuthAPPURL = "https://open.weixin.qq.com/connect/oauth2/authorize"
|
|
TokenURL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"
|
|
UserInfoURL = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo"
|
|
UserDetailURL = "https://qyapi.weixin.qq.com/cgi-bin/user/get"
|
|
// DepartmentListURL https://developer.work.weixin.qq.com/document/path/90344
|
|
DepartmentListURL = "https://qyapi.weixin.qq.com/cgi-bin/department/list"
|
|
// UserListUrl https://developer.work.weixin.qq.com/document/path/90337
|
|
UserListUrl = "https://qyapi.weixin.qq.com/cgi-bin/user/list"
|
|
callbackPath = "/share/pro/v1/openapi/wecom/callback"
|
|
)
|
|
|
|
// Client 企业微信客户端
|
|
type Client struct {
|
|
context context.Context
|
|
cache *cache.Cache
|
|
httpClient *http.Client
|
|
oauthConfig *oauth2.Config
|
|
logger *log.Logger
|
|
corpID string
|
|
agentID string
|
|
}
|
|
|
|
type TokenResponse struct {
|
|
ErrCode int `json:"errcode"`
|
|
ErrMsg string `json:"errmsg"`
|
|
AccessToken string `json:"access_token"`
|
|
ExpiresIn int `json:"expires_in"`
|
|
}
|
|
|
|
type UserInfoResponse struct {
|
|
ErrCode int `json:"errcode"`
|
|
ErrMsg string `json:"errmsg"`
|
|
UserID string `json:"userid"`
|
|
UserTicket string `json:"user_ticket"`
|
|
OpenID string `json:"openid"`
|
|
ExternalUserid string `json:"external_userid"`
|
|
}
|
|
|
|
type UserDetailResponse struct {
|
|
Errcode int `json:"errcode"`
|
|
Errmsg string `json:"errmsg"`
|
|
Userid string `json:"userid"`
|
|
Name string `json:"name"`
|
|
Mobile string `json:"mobile"`
|
|
Gender string `json:"gender"`
|
|
Email string `json:"email"`
|
|
Avatar string `json:"avatar"`
|
|
OpenUserid string `json:"open_userid"`
|
|
}
|
|
type DepartmentListResponse struct {
|
|
Errcode int `json:"errcode"`
|
|
Errmsg string `json:"errmsg"`
|
|
Department []struct {
|
|
Id int `json:"id"`
|
|
Name string `json:"name"`
|
|
NameEn string `json:"name_en"`
|
|
DepartmentLeader []string `json:"department_leader"`
|
|
Parentid int `json:"parentid"`
|
|
Order int `json:"order"`
|
|
} `json:"department"`
|
|
}
|
|
|
|
type UserListResponse struct {
|
|
Errcode int `json:"errcode"`
|
|
Errmsg string `json:"errmsg"`
|
|
Userlist []struct {
|
|
Name string `json:"name"`
|
|
Department []int `json:"department"`
|
|
Position string `json:"position"`
|
|
Status int `json:"status"`
|
|
Email string `json:"email"`
|
|
Avatar string `json:"avatar"`
|
|
Enable int `json:"enable"`
|
|
Isleader int `json:"isleader"`
|
|
Extattr struct {
|
|
Attrs []interface{} `json:"attrs"`
|
|
} `json:"extattr"`
|
|
HideMobile int `json:"hide_mobile"`
|
|
Telephone string `json:"telephone"`
|
|
Order []int `json:"order"`
|
|
ExternalProfile struct {
|
|
ExternalAttr []interface{} `json:"external_attr"`
|
|
ExternalCorpName string `json:"external_corp_name"`
|
|
} `json:"external_profile"`
|
|
MainDepartment int `json:"main_department"`
|
|
Alias string `json:"alias"`
|
|
IsLeaderInDept []int `json:"is_leader_in_dept"`
|
|
Userid string `json:"userid"`
|
|
DirectLeader []interface{} `json:"direct_leader"`
|
|
} `json:"userlist"`
|
|
}
|
|
|
|
func NewClient(ctx context.Context, logger *log.Logger, corpID, corpSecret, agentID, baseUrl string, cache *cache.Cache, isApp bool) (*Client, error) {
|
|
redirectURI, err := url.JoinPath(baseUrl, callbackPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
authUrl := AuthWebURL
|
|
if isApp {
|
|
authUrl = AuthAPPURL
|
|
}
|
|
|
|
oauthConfig := &oauth2.Config{
|
|
ClientID: fmt.Sprintf("%s-%s", corpID, agentID),
|
|
ClientSecret: corpSecret,
|
|
RedirectURL: redirectURI,
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: authUrl,
|
|
TokenURL: TokenURL,
|
|
},
|
|
Scopes: []string{"snsapi_privateinfo"},
|
|
}
|
|
|
|
return &Client{
|
|
context: ctx,
|
|
httpClient: &http.Client{},
|
|
cache: cache,
|
|
logger: logger.WithModule("wecom.client"),
|
|
oauthConfig: oauthConfig,
|
|
corpID: corpID,
|
|
agentID: agentID,
|
|
}, nil
|
|
}
|
|
|
|
// GenerateAuthURL 生成授权 URL
|
|
func (c *Client) GenerateAuthURL(state string) string {
|
|
params := url.Values{}
|
|
params.Set("appid", c.corpID)
|
|
params.Set("redirect_uri", c.oauthConfig.RedirectURL)
|
|
params.Set("response_type", "code")
|
|
params.Set("scope", "snsapi_privateinfo")
|
|
params.Set("login_type", "CorpApp")
|
|
params.Set("agentid", c.agentID)
|
|
params.Set("state", state)
|
|
|
|
authUrl := fmt.Sprintf("%s?%s", c.oauthConfig.Endpoint.AuthURL, params.Encode())
|
|
if c.oauthConfig.Endpoint.AuthURL == AuthAPPURL {
|
|
authUrl += "#wechat_redirect"
|
|
}
|
|
return authUrl
|
|
}
|
|
|
|
// GetAccessToken 获取企业微信访问令牌
|
|
func (c *Client) GetAccessToken(ctx context.Context) (string, error) {
|
|
|
|
cacheKey := fmt.Sprintf("wecom-access-token:%s", c.oauthConfig.ClientID)
|
|
cachedData, err := c.cache.Get(ctx, cacheKey).Result()
|
|
if err == nil && cachedData != "" {
|
|
return cachedData, nil
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("corpid", c.corpID)
|
|
params.Set("corpsecret", c.oauthConfig.ClientSecret)
|
|
|
|
resp, err := c.httpClient.Get(fmt.Sprintf("%s?%s", TokenURL, params.Encode()))
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to get access token: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
var tokenResp TokenResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
|
|
return "", fmt.Errorf("failed to decode token response: %w", err)
|
|
}
|
|
|
|
if tokenResp.ErrCode != 0 {
|
|
return "", fmt.Errorf("failed to get access token: %s", tokenResp.ErrMsg)
|
|
}
|
|
|
|
if err := c.cache.Set(ctx, cacheKey, tokenResp.AccessToken, time.Duration(tokenResp.ExpiresIn-300)*time.Second).Err(); err != nil {
|
|
c.logger.Warn("failed to set cache", log.Error(err))
|
|
}
|
|
|
|
return tokenResp.AccessToken, nil
|
|
}
|
|
|
|
// GetUserInfoByCode 通过授权码获取用户信息
|
|
func (c *Client) GetUserInfoByCode(ctx context.Context, code string) (*UserDetailResponse, error) {
|
|
|
|
accessToken, err := c.GetAccessToken(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get access token: %w", err)
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("access_token", accessToken)
|
|
params.Set("code", code)
|
|
|
|
userInfoURL := fmt.Sprintf("%s?%s", UserInfoURL, params.Encode())
|
|
|
|
c.logger.Debug("GetUserInfoByCode", log.Any("userInfoURL", userInfoURL))
|
|
|
|
resp, err := c.httpClient.Get(userInfoURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user info: %w", err)
|
|
}
|
|
|
|
rawBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read body: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
c.logger.Debug("GetUserInfoByCode raw resp:", log.Any("raw", string(rawBody)))
|
|
|
|
resp.Body = io.NopCloser(bytes.NewReader(rawBody))
|
|
|
|
var userInfoResp UserInfoResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&userInfoResp); err != nil {
|
|
return nil, fmt.Errorf("failed to decode user info response: %w", err)
|
|
}
|
|
|
|
c.logger.Debug("GetUserInfoByCode resp:", log.Any("resp", userInfoResp))
|
|
|
|
if userInfoResp.ErrCode != 0 {
|
|
return nil, fmt.Errorf("failed to get user info: %s", userInfoResp.ErrMsg)
|
|
}
|
|
|
|
detailParams := url.Values{}
|
|
detailParams.Set("access_token", accessToken)
|
|
detailParams.Set("userid", userInfoResp.UserID)
|
|
|
|
userDetailURL := fmt.Sprintf("%s?%s", UserDetailURL, detailParams.Encode())
|
|
|
|
detailResp, err := c.httpClient.Get(userDetailURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user detail: %w", err)
|
|
}
|
|
defer detailResp.Body.Close()
|
|
|
|
var UserDetailResp UserDetailResponse
|
|
if err := json.NewDecoder(detailResp.Body).Decode(&UserDetailResp); err != nil {
|
|
return nil, fmt.Errorf("failed to decode user detail response: %w", err)
|
|
}
|
|
|
|
c.logger.Debug("GetUserInfoByCode detail info", log.Any("resp", UserDetailResp))
|
|
|
|
if UserDetailResp.Errcode != 0 {
|
|
return nil, fmt.Errorf("failed to get user detail: %s", UserDetailResp.Errmsg)
|
|
}
|
|
|
|
return &UserDetailResp, nil
|
|
}
|
|
|
|
func (c *Client) GetDepartmentList(ctx context.Context) (*DepartmentListResponse, error) {
|
|
|
|
accessToken, err := c.GetAccessToken(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get access token: %w", err)
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("access_token", accessToken)
|
|
|
|
departmentListURL := fmt.Sprintf("%s?%s", DepartmentListURL, params.Encode())
|
|
|
|
resp, err := c.httpClient.Get(departmentListURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get department list: %w", err)
|
|
}
|
|
|
|
rawBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read body: %w", err)
|
|
}
|
|
c.logger.Debug("GetDepartmentList raw resp:", log.Any("raw", string(rawBody)))
|
|
defer resp.Body.Close()
|
|
|
|
resp.Body = io.NopCloser(bytes.NewReader(rawBody))
|
|
|
|
var departmentListResponse DepartmentListResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&departmentListResponse); err != nil {
|
|
return nil, fmt.Errorf("failed to decode department list response: %w", err)
|
|
}
|
|
|
|
c.logger.Debug("GetDepartmentList resp:", log.Any("resp", departmentListResponse))
|
|
|
|
if departmentListResponse.Errcode != 0 {
|
|
return nil, fmt.Errorf("failed to get user info: %s", departmentListResponse.Errmsg)
|
|
}
|
|
|
|
return &departmentListResponse, nil
|
|
}
|
|
|
|
func (c *Client) GetUserList(ctx context.Context, deptID string) (*UserListResponse, error) {
|
|
|
|
accessToken, err := c.GetAccessToken(ctx)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get access token: %w", err)
|
|
}
|
|
|
|
params := url.Values{}
|
|
params.Set("access_token", accessToken)
|
|
params.Set("department_id", deptID)
|
|
|
|
userListUrl := fmt.Sprintf("%s?%s", UserListUrl, params.Encode())
|
|
|
|
resp, err := c.httpClient.Get(userListUrl)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get user list: %w", err)
|
|
}
|
|
|
|
rawBody, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read body: %w", err)
|
|
}
|
|
|
|
c.logger.Debug("GetUserList raw resp:", log.Any("raw", string(rawBody)))
|
|
|
|
resp.Body = io.NopCloser(bytes.NewReader(rawBody))
|
|
|
|
var userListResponse UserListResponse
|
|
if err := json.NewDecoder(resp.Body).Decode(&userListResponse); err != nil {
|
|
return nil, fmt.Errorf("failed to decode user list response: %w", err)
|
|
}
|
|
|
|
c.logger.Debug("GetUserList resp:", log.Any("resp", userListResponse))
|
|
|
|
if userListResponse.Errcode != 0 {
|
|
return nil, fmt.Errorf("failed to get user info: %s", userListResponse.Errmsg)
|
|
}
|
|
|
|
return &userListResponse, nil
|
|
}
|