From 5936096fc0d35ef12c583465cd6a4489135aa42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Flc=E3=82=9B?= Date: Thu, 3 Aug 2023 14:26:26 +0800 Subject: [PATCH] refactor: youdu 2.x (#6) --- .gitignore | 2 + README.md | 45 ++--- access_token.go | 90 ---------- auth.go | 121 -------------- client.go | 125 ++++++++++++++ config.go | 52 ------ dept.go | 68 +++----- encryptor.go | 95 +++++------ errors.go | 19 +++ feature.md | 54 +++--- go.mod | 11 +- go.sum | 18 +- group.go | 396 -------------------------------------------- http.go | 53 ------ internal/aes/aes.go | 138 +++++++++++++++ media.go | 243 --------------------------- message.go | 231 +++++++++++++------------- message/message.go | 163 ------------------ options.go | 111 +++++++++++++ response.go | 45 ----- session.go | 219 ------------------------ session/message.go | 65 -------- session/session.go | 12 -- store.go | 5 + token.go | 60 +++++++ user.go | 225 +++++-------------------- youdu.go | 97 ----------- 27 files changed, 746 insertions(+), 2017 deletions(-) delete mode 100644 access_token.go delete mode 100644 auth.go create mode 100644 client.go delete mode 100644 config.go create mode 100644 errors.go delete mode 100644 group.go delete mode 100644 http.go create mode 100644 internal/aes/aes.go delete mode 100644 media.go delete mode 100644 message/message.go create mode 100644 options.go delete mode 100644 response.go delete mode 100644 session.go delete mode 100644 session/message.go delete mode 100644 session/session.go create mode 100644 store.go create mode 100644 token.go delete mode 100644 youdu.go diff --git a/.gitignore b/.gitignore index 67ae696..02cd3bf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,5 @@ vendor/ .idea youdu_test.go +client_test.go +_example \ No newline at end of file diff --git a/README.md b/README.md index 163ae5d..1bafca7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

Youdu Go SDK

Lint - GoDoc + GoDoc Go Report Card Language MIT license @@ -16,14 +16,10 @@ [Features](./feature.md) -### Prerequisites - -- Golang Version >= 1.16 - ### Installation ```bash -go get github.com/addcnos/youdu +go get github.com/addcnos/youdu/v2 ``` ### Usage @@ -34,27 +30,32 @@ Please refer to the [Documentation](./docs) package main import ( - "github.com/addcnos/youdu" - "github.com/addcnos/youdu/message" -) + "context" + "fmt" -func main() { + "github.com/addcnos/youdu/v2" +) - yd := youdu.New(&youdu.Config{ - Api: "http://domain.com/api", // youdu api host - Buin: 1111111, // 企业 buin 码 - AppId: "22222222222222", // 应用 appId - AesKey: "3444444444444444444444444444444444", // 应用 AesKey +func main() { + client := youdu.NewClient(&youdu.Config{ + Addr: "http://examaple", + Buin: 111222333, + AppId: "111222333", + AesKey: "111333445", }) - yd.Message().Send(&message.TextMessage{ - ToUser: "user1|user2", // 指定用户 - ToDept: "dep1|dep2", // 指定部门 - MsgType: message.MsgTypeText, - Text: &message.TextItem{ - Content: "content", + resp, err := client.SendTextMessage(context.Background(), youdu.TextMessageRequest{ + ToUser: "11111", + MsgType: youdu.MsgTypeText, + Text: youdu.MessageText{ + Content: "hello", }, }) + if err != nil { + panic(err) + } + + fmt.Println(resp) } ``` @@ -66,4 +67,4 @@ Very welcome to join us! Raise an [Issue](https://github.com/addcnos/youdu/issue ## License -[MIT License](LICENSE) © 2022 addcnos \ No newline at end of file +[MIT License](LICENSE) © 2022-2023 addcnos \ No newline at end of file diff --git a/access_token.go b/access_token.go deleted file mode 100644 index 838387a..0000000 --- a/access_token.go +++ /dev/null @@ -1,90 +0,0 @@ -package youdu - -import ( - "errors" - "strconv" - "time" -) - -const getTokenUrl = "/cgi/gettoken" - -type accessToken struct { - accessToken string - expire time.Time -} - -type accessTokenProvider struct { - config *Config - accessToken accessToken -} - -func NewAccessTokenProvider(config *Config) *accessTokenProvider { - return &accessTokenProvider{ - config: config, - } -} - -func (p *accessTokenProvider) GetAccessToken() (string, error) { - // from memory - if at := p.accessToken.GetAccessToken(); at != "" && p.accessToken.Expire().After(time.Now()) { - return at, nil - } - - // from cache - // todo: support cache - - return p.Refresh() -} - -func (p *accessTokenProvider) Refresh() (string, error) { - // encrypt - encrypt, err := p.config.GetEncryptor().Encrypt(strconv.Itoa(int(time.Now().Unix()))) - if err != nil { - return "", err - } - - resp, err := p.config.GetHttp().Post(getTokenUrl, map[string]interface{}{ - "appId": p.config.AppId, - "buin": p.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return "", err - } - - if !resp.IsSuccess() { - return "", errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return "", err - } - - decrypt, err := p.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return "", err - } - - var v struct { - AccessToken string `json:"accessToken"` - ExpireIn int `json:"expireIn"` - } - if err := decrypt.Unmarshal(&v); err != nil { - return "", err - } - - p.accessToken.accessToken = v.AccessToken - p.accessToken.expire = time.Now().Add(time.Duration(v.ExpireIn-600) * time.Second) // 提前10分钟失效 - - return p.accessToken.accessToken, nil -} - -func (a accessToken) GetAccessToken() string { - return a.accessToken -} - -func (a accessToken) Expire() time.Time { - return a.expire -} diff --git a/auth.go b/auth.go deleted file mode 100644 index f98a7fe..0000000 --- a/auth.go +++ /dev/null @@ -1,121 +0,0 @@ -package youdu - -import ( - "crypto/md5" - "encoding/json" - "errors" - "fmt" - "strconv" -) - -const ( - identifyUrl = "/cgi/identify" - userSetAuthUrl = "/cgi/user/setauth" -) - -type Auth struct { - config *Config -} - -func NewAuth(config *Config) *Auth { - return &Auth{ - config: config, - } -} - -type IdentifyResp struct { - Buin int `json:"buin"` - Status struct { - Code int `json:"code"` - Message string `json:"message"` - CreatedAt string `json:"createdAt"` - } `json:"status"` - UserInfo struct { - Gid int `json:"gid"` - Account string `json:"account"` - ChsName string `json:"chsName"` - EngName string `json:"engName"` - Gender int `json:"gender"` - OrgId int `json:"orgId"` - Mobile string `json:"mobile"` - Phone string `json:"phone"` - Email string `json:"email"` - CustomAttr string `json:"customAttr"` - } `json:"userInfo"` -} - -// Identify 单点登录 -func (a *Auth) Identify(token string) (i IdentifyResp, err error) { - resp, err := a.config.GetHttp().Get(identifyUrl, map[string]string{ - "token": token, - }) - if err != nil { - return - } - - if !resp.IsSuccess() { - err = errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - return - } - - if err = json.Unmarshal(resp.Body(), &i); err != nil { - return - } - - return -} - -type SetAuthResp struct { - FromUser string `json:"fromUser"` - CreateTime int `json:"createTime"` - PackageId int `json:"packageId"` - MsgType string `json:"msgType"` - Passwd string `json:"passwd"` -} - -// SetAuth 第三方认证-设置认证信息 -func (a *Auth) SetAuth(userId, password string) (bool, error) { - accessToken, err := a.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return false, err - } - - bodyJson, err := json.Marshal(map[string]interface{}{ - "userId": userId, - "authType": 2, - "passwd": fmt.Sprintf("%x", md5.Sum([]byte(password))), - }) - if err != nil { - return false, err - } - - encrypt, err := a.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return false, err - } - - resp, err := a.config.GetHttp().Post(userSetAuthUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": a.config.AppId, - "buin": a.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return false, err - } - - if !resp.IsSuccess() { - return false, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return false, err - } - - if jsonRet["errcode"].(float64) != 0 { - return false, errors.New(jsonRet["errMsg"].(string)) - } - - return true, nil -} diff --git a/client.go b/client.go new file mode 100644 index 0000000..7b53124 --- /dev/null +++ b/client.go @@ -0,0 +1,125 @@ +package youdu + +import ( + "context" + "encoding/json" + "io" + "net/http" +) + +type Config struct { + Addr string + Buin int + AppId string + AesKey string +} + +type Client struct { + config *Config + encryptor *Encryptor + token *token +} + +func NewClient(config *Config) *Client { + return &Client{ + config: config, + encryptor: NewEncryptorWithConfig(config), + } +} + +func (c *Client) newRequest(ctx context.Context, method string, path string, opts ...requestOption) (req *http.Request, err error) { + var ( + opt = newRequestOptions(opts...) + url = c.config.Addr + path + ) + + // body + bodyReader, err := c.encodeRequestBody(opt) + if err != nil { + return nil, err + } + + // access_token + if opt.needAccessToken { + if token, err := c.GetToken(ctx); err != nil { + return nil, err + } else { + opt.params.Add("accessToken", token) + } + } + + req, err = http.NewRequestWithContext(ctx, method, url+"?"+opt.params.Encode(), bodyReader) + return +} + +func (c *Client) encodeRequestBody(opt *requestOptions) (io.Reader, error) { + if opt.body == nil { + return nil, nil + } + + if !opt.needEncrypt { + return opt.bodyReader(opt.body) + } + + reqBytes, err := json.Marshal(opt.body) + if err != nil { + return nil, err + } + + cipherText, err := c.encryptor.Encrypt(reqBytes) + if err != nil { + return nil, err + } + + return opt.bodyReader(Request{ + Buin: c.config.Buin, + AppId: c.config.AppId, + Encrypt: cipherText, + }) + +} + +func (c *Client) sendRequest(req *http.Request, resp interface{}, opts ...responseOption) error { + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + return c.decodeResponse(res.Body, resp, opts...) +} + +func (c *Client) decodeResponse(body io.Reader, resp interface{}, opts ...responseOption) error { + opt := newResponseOptions(opts...) + + if !opt.needDecrypt { + return json.NewDecoder(body).Decode(resp) + } + + return c.decodeResponseWithDecrypt(body, resp, opts...) +} + +func (c *Client) decodeResponseWithDecrypt(body io.Reader, resp interface{}, opts ...responseOption) error { + var r Response + if err := json.NewDecoder(body).Decode(&r); err != nil { + return err + } + + if r.ErrCode != 0 { + return newError(r.ErrCode, r.ErrMsg) + } + if r.Encrypt == "" { + return newError(-1, "encrypt is empty") + } + + rawData, err := c.encryptor.Decrypt(r.Encrypt) + if err != nil { + return err + } + + if rawData.Data == nil { + return newError(-1, "data is nil") + } + + return json.Unmarshal(rawData.Data, resp) +} diff --git a/config.go b/config.go deleted file mode 100644 index 729936e..0000000 --- a/config.go +++ /dev/null @@ -1,52 +0,0 @@ -package youdu - -import "strings" - -type Config struct { - Api string - Buin int - AppId string - AesKey string - Path string - - encryptor *encryptor - http *Http - accessTokenProvider *accessTokenProvider -} - -func (c *Config) GetEncryptor() *encryptor { - if c.encryptor == nil { - c.encryptor = NewEncryptor(c) - } - - return c.encryptor -} - -func (c *Config) GetHttp() *Http { - if c.http == nil { - c.http = NewHttp(c) - } - - return c.http -} - -func (c *Config) GetAccessTokenProvider() *accessTokenProvider { - if c.accessTokenProvider == nil { - c.accessTokenProvider = NewAccessTokenProvider(c) - } - - return c.accessTokenProvider -} - -// GetPath 返回系统默认路径 -func (c *Config) GetPath() string { - if c.Path == "" { - c.Path = c.GetDefaultPath() - } - - return strings.TrimRight(c.Path, "/") -} - -func (c *Config) GetDefaultPath() string { - return "/tmp" -} diff --git a/dept.go b/dept.go index 9d5d127..83dfeaa 100644 --- a/dept.go +++ b/dept.go @@ -1,65 +1,39 @@ package youdu import ( - "errors" + "context" + "net/http" "strconv" ) -const ( - deptListUrl = "/cgi/dept/list" -) - -type Dept struct { - config *Config +type DeptList struct { + ID interface{} `json:"id"` + Name string `json:"name"` + ParentId interface{} `json:"parentId"` + SortId interface{} `json:"sortId"` } -type DeptItem struct { - Id int `json:"id"` - Name string `json:"name"` - ParentId int `json:"parentId"` - SortId int `json:"sortId"` +type DeptListResponse struct { + DeptList []DeptList `json:"deptList"` } -func NewDept(config *Config) *Dept { - return &Dept{ - config: config, +func (c *Client) GetDeptList(ctx context.Context, id ...int) (response DeptListResponse, err error) { + opts := []requestOption{ + withRequestAccessToken(), + withRequestEncrypt(), } -} -// GetList 获取部门列表 -func (d *Dept) GetList(depId int) ([]DeptItem, error) { - accessToken, err := d.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return nil, err + if len(id) > 0 { + opts = append(opts, withRequestParamsKV("id", strconv.Itoa(id[0]))) + } else { + opts = append(opts, withRequestParamsKV("id", "0")) } - resp, err := d.config.GetHttp().Get(deptListUrl, map[string]string{ - "id": strconv.Itoa(depId), - "accessToken": accessToken, - }) - + req, err := c.newRequest(ctx, http.MethodGet, "/cgi/dept/list", opts...) if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return nil, err - } - - decrypt, err := d.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return nil, err - } - - var v map[string][]DeptItem - if err := decrypt.Unmarshal(&v); err != nil { - return nil, err + return } - return v["deptList"], nil + err = c.sendRequest(req, &response, withResponseDecrypt()) + return } diff --git a/encryptor.go b/encryptor.go index 042d381..5c8eeb7 100644 --- a/encryptor.go +++ b/encryptor.go @@ -7,93 +7,80 @@ import ( "crypto/rand" "encoding/base64" "encoding/binary" - "encoding/json" "errors" "io" ) -type DecryptResult struct { +type RawData struct { AppId string - Data string + Data []byte Length int32 } -func (d *DecryptResult) Unmarshal(v interface{}) error { - return json.Unmarshal([]byte(d.Data), &v) +type Encryptor struct { + key []byte + appid string + pkcs7 *pkcs7 } -// encryptor is used to encrypt and decrypt messages. -// see: https://gist.github.com/STGDanny/03acf29a90684c2afc9487152324e832 -type encryptor struct { - config *Config - pkcs7 *Pkcs7 -} - -func NewEncryptor(config *Config) *encryptor { - return &encryptor{ - config: config, - pkcs7: NewPkcs7(), +func NewEncryptor(key []byte, appid string) *Encryptor { + return &Encryptor{ + key: key, + appid: appid, + pkcs7: newPkcs7(), } } -func (e *encryptor) Encrypt(plaintext string) (string, error) { - // key - key, err := base64.StdEncoding.DecodeString(e.config.AesKey) +func NewEncryptorWithConfig(config *Config) *Encryptor { + key, err := base64.StdEncoding.DecodeString(config.AesKey) if err != nil { - return "", err + panic(err) } - // plainText + return NewEncryptor(key, config.AppId) +} + +func (e *Encryptor) Encrypt(plaintext []byte) (string, error) { plainText := make([]byte, 0) randBs := make([]byte, 16) - _, err = io.ReadFull(rand.Reader, randBs) + _, err := io.ReadFull(rand.Reader, randBs) if err != nil { return "", err } lenBs := make([]byte, 4) - binary.BigEndian.PutUint32(lenBs, uint32(len([]byte(plaintext)))) + binary.BigEndian.PutUint32(lenBs, uint32(len(plaintext))) plainText = append(plainText, randBs...) plainText = append(plainText, lenBs...) - plainText = append(plainText, []byte(plaintext)...) - plainText = append(plainText, []byte(e.config.AppId)...) + plainText = append(plainText, plaintext...) + plainText = append(plainText, []byte(e.appid)...) - // encrypt - block, err := aes.NewCipher(key) + block, err := aes.NewCipher(e.key) if err != nil { return "", err } - blockMode := cipher.NewCBCEncrypter(block, key[:block.BlockSize()]) - plainText = e.pkcs7.Padding(plainText) + blockMode := cipher.NewCBCEncrypter(block, e.key[:block.BlockSize()]) + plainText = e.pkcs7.padding(plainText) cipherText := make([]byte, len(plainText)) blockMode.CryptBlocks(cipherText, plainText) return base64.StdEncoding.EncodeToString(cipherText), nil } -func (e *encryptor) Decrypt(ciphertext string) (*DecryptResult, error) { - // key - key, err := base64.StdEncoding.DecodeString(e.config.AesKey) - if err != nil { - return nil, err - } - - // cipherText +func (e *Encryptor) Decrypt(ciphertext string) (*RawData, error) { cipherText, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return nil, err } - // len valid - if len(cipherText)%len(key) != 0 { + if len(cipherText)%len(e.key) != 0 { return nil, errors.New("invalid ciphertext") } - // aes decrypt - block, err := aes.NewCipher(key) + block, err := aes.NewCipher(e.key) if err != nil { return nil, err } @@ -106,14 +93,14 @@ func (e *encryptor) Decrypt(ciphertext string) (*DecryptResult, error) { blockMode := cipher.NewCBCDecrypter(block, iv) plainText := make([]byte, len(cipherText)) blockMode.CryptBlocks(plainText, cipherText) - plainText = e.pkcs7.Unpadding(plainText) + plainText = e.pkcs7.unpadding(plainText) - // rawMessage - result := &DecryptResult{} + result := &RawData{} if err := binary.Read(bytes.NewBuffer(plainText[16:20]), binary.BigEndian, &result.Length); err != nil { return nil, err } - result.Data = string(plainText[20 : 20+result.Length]) + + result.Data = plainText[20 : 20+result.Length] result.AppId = string(plainText[20+result.Length:]) if len(plainText) < int(20+result.Length) { return nil, errors.New("invalid ciphertext") @@ -122,20 +109,20 @@ func (e *encryptor) Decrypt(ciphertext string) (*DecryptResult, error) { return result, err } -// Pkcs7 is used to padding and unpadding messages. -type Pkcs7 struct { +// pkcs7 is used to padding and unpadding messages. +type pkcs7 struct { blockSize int } -// NewPkcs7 is used to create a new Pkcs7. -func NewPkcs7() *Pkcs7 { - return &Pkcs7{ +// newPkcs7 is used to create a new pkcs7. +func newPkcs7() *pkcs7 { + return &pkcs7{ blockSize: 32, } } -// Padding is used to padding messages. -func (p *Pkcs7) Padding(content []byte) []byte { +// padding is used to padding messages. +func (p *pkcs7) padding(content []byte) []byte { padding := p.blockSize - (len(content) % p.blockSize) if padding == 0 { @@ -147,8 +134,8 @@ func (p *Pkcs7) Padding(content []byte) []byte { return append(content, padtext...) } -// Unpadding is used to unpadding messages. -func (p *Pkcs7) Unpadding(content []byte) []byte { +// unpadding is used to unpadding messages. +func (p *pkcs7) unpadding(content []byte) []byte { if len(content) == 0 { return nil } diff --git a/errors.go b/errors.go new file mode 100644 index 0000000..b8f4da1 --- /dev/null +++ b/errors.go @@ -0,0 +1,19 @@ +package youdu + +import "fmt" + +type Error struct { + Code int `json:"errcode"` + Message string `json:"errmsg"` +} + +func (e *Error) Error() string { + return fmt.Sprintf("code: %d, message: %s", e.Code, e.Message) +} + +func newError(code int, message string) error { + return &Error{ + Code: code, + Message: message, + } +} diff --git a/feature.md b/feature.md index 7305d75..7befe42 100644 --- a/feature.md +++ b/feature.md @@ -10,9 +10,9 @@ - [x] 图文消息 - [x] 隐式链接 - [x] 外链消息 -- [x] 系统消息 -- [x] 短信消息(短信网关收到的手机回复的短信) -- [x] 邮件消息 +- [ ] 系统消息 +- [ ] 短信消息 +- [ ] 邮件消息 ### 设置角标 @@ -20,27 +20,27 @@ ### 发送弹窗消息 -- [x] 发送弹窗消息 +- [ ] 发送弹窗消息 ## 单点登录 -- [x] 单点登录 +- [ ] 单点登录 ## 会话管理 -- [x] 创建会话 -- [x] 获取会话 -- [x] 修改会话 +- [ ] 创建会话 +- [ ] 获取会话 +- [ ] 修改会话 ## 会话消息 ### 发送消息 -- [x] 文本消息 -- [x] 图片消息 -- [x] 文件消息 -- [x] 语音消息 -- [x] 视频消息 +- [ ] 文本消息 +- [ ] 图片消息 +- [ ] 文件消息 +- [ ] 语音消息 +- [ ] 视频消息 ## 自定义菜单 @@ -66,27 +66,27 @@ - [ ] 批量删除用户 - [x] 获取用户信息 - [x] 获取部门用户详细信息 -- [x] 获取部门用户 +- [ ] 获取部门用户 - [ ] 设置用户头像 - [ ] 获取用户头像 - [ ] 更新用户拓展属性字段 -- [x] 查询用户激活状态 +- [ ] 查询用户激活状态 - [ ] 修改用户激活状态 ### 第三方认证 -- [x] 设置认证信息 +- [ ] 设置认证信息 ### 群管理 -- [x] 创建群 -- [x] 删除群 -- [x] 修改群名称 -- [x] 查看群信息 -- [x] 群列表 -- [x] 添加群成员 -- [x] 删除群成员 -- [x] 查询用户是否为群成员 +- [ ] 创建群 +- [ ] 删除群 +- [ ] 修改群名称 +- [ ] 查看群信息 +- [ ] 群列表 +- [ ] 添加群成员 +- [ ] 删除群成员 +- [ ] 查询用户是否为群成员 ### 全量覆盖 @@ -96,6 +96,6 @@ ## 素材管理 -- [x] 上传素材文件 -- [x] 下载素材文件 -- [x] 查询素材文件信息 \ No newline at end of file +- [ ] 上传素材文件 +- [ ] 下载素材文件 +- [ ] 查询素材文件信息 \ No newline at end of file diff --git a/go.mod b/go.mod index 20bdd32..2c1d60d 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,11 @@ -module github.com/addcnos/youdu +module github.com/addcnos/youdu/v2 -go 1.16 +go 1.20 + +require github.com/stretchr/testify v1.8.4 require ( - github.com/go-resty/resty/v2 v2.7.0 - github.com/stretchr/testify v1.8.0 + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ee8d280..fa4b6e6 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,10 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY= -github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM= -golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/group.go b/group.go deleted file mode 100644 index 2af25d6..0000000 --- a/group.go +++ /dev/null @@ -1,396 +0,0 @@ -package youdu - -import ( - "encoding/json" - "errors" - "strconv" -) - -const ( - groupCreateUrl = "/cgi/group/create" - groupDeleteUrl = "/cgi/group/delete" - groupUpdateUrl = "/cgi/group/update" - groupInfoUrl = "/cgi/group/info" - groupListUrl = "/cgi/group/list" - groupAddMemberUrl = "/cgi/group/addmember" - groupDelMemberUrl = "/cgi/group/delmember" - groupIsMemberUrl = "/cgi/group/ismember" -) - -type Group struct { - config *Config -} - -func NewGroup(config *Config) *Group { - return &Group{ - config: config, - } -} - -// Create 创建一个群组 -func (g *Group) Create(name string) (string, error) { - accessToken, err := g.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return "", err - } - - bodyJson, err := json.Marshal(map[string]interface{}{ - "name": name, - }) - if err != nil { - return "", err - } - - encrypt, err := g.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return "", err - } - - resp, err := g.config.GetHttp().Post(groupCreateUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": g.config.AppId, - "buin": g.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return "", err - } - - if !resp.IsSuccess() { - return "", errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return "", err - } - - decrypt, err := g.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return "", err - } - - var v map[string]string - if err := decrypt.Unmarshal(&v); err != nil { - return "", err - } - - return v["id"], nil -} - -func (g *Group) Delete(groupId string) (bool, error) { - accessToken, err := g.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return false, err - } - - bodyJson, err := json.Marshal(map[string]interface{}{ - "id": groupId, - }) - if err != nil { - return false, err - } - - encrypt, err := g.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return false, err - } - - resp, err := g.config.GetHttp().Post(groupDeleteUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": g.config.AppId, - "buin": g.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return false, err - } - - if !resp.IsSuccess() { - return false, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return false, err - } - - if jsonRet["errcode"].(float64) != 0 { - return false, errors.New(jsonRet["errMsg"].(string)) - } - - return true, nil -} - -func (g *Group) Update(groupId, groupName string) (bool, error) { - accessToken, err := g.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return false, err - } - - bodyJson, err := json.Marshal(map[string]interface{}{ - "id": groupId, - "name": groupName, - }) - if err != nil { - return false, err - } - - encrypt, err := g.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return false, err - } - - resp, err := g.config.GetHttp().Post(groupUpdateUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": g.config.AppId, - "buin": g.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return false, err - } - - if !resp.IsSuccess() { - return false, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return false, err - } - - if jsonRet["errcode"].(float64) != 0 { - return false, errors.New(jsonRet["errMsg"].(string)) - } - - return true, nil -} - -type GroupInfo struct { - Id string `json:"id"` - Name string `json:"name"` - Admins interface{} `json:"admins"` - BelongDeptId int `json:"belongDeptId"` - IsDeptGroup bool `json:"isDeptGroup"` - Master int `json:"master"` - Members []struct { - Account string `json:"account"` - Name string `json:"name"` - Mobile string `json:"mobile"` - } `json:"members"` -} - -func (g *Group) Info(groupId string) (*GroupInfo, error) { - accessToken, err := g.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return nil, err - } - - resp, err := g.config.GetHttp().Get(groupInfoUrl+"?accessToken="+accessToken, map[string]string{ - "id": groupId, - }) - - if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return nil, err - } - - decrypt, err := g.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return nil, err - } - - var v *GroupInfo - if err = decrypt.Unmarshal(&v); err != nil { - return nil, err - } - - return v, nil -} - -type GroupItem struct { - Id string `json:"id"` - Name string `json:"name"` - Version int `json:"version"` - IsDeptGroup bool `json:"isDeptGroup"` - BelongDeptId int `json:"belongDeptId"` -} - -func (g *Group) List(userId ...string) ([]GroupItem, error) { - accessToken, err := g.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return nil, err - } - - params := map[string]string{} - if len(userId) > 0 { - params["userId"] = userId[0] - } - - resp, err := g.config.GetHttp().Get(groupListUrl+"?accessToken="+accessToken, params) - - if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return nil, err - } - - decrypt, err := g.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return nil, err - } - - var v map[string][]GroupItem - if err := decrypt.Unmarshal(&v); err != nil { - return nil, err - } - - return v["groupList"], nil -} - -func (g *Group) AddMember(groupId string, userId ...string) (bool, error) { - accessToken, err := g.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return false, err - } - - bodyJson, err := json.Marshal(map[string]interface{}{ - "id": groupId, - "userList": userId, - }) - if err != nil { - return false, err - } - - encrypt, err := g.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return false, err - } - - resp, err := g.config.GetHttp().Post(groupAddMemberUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": g.config.AppId, - "buin": g.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return false, err - } - - if !resp.IsSuccess() { - return false, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return false, err - } - - if jsonRet["errcode"].(float64) != 0 { - return false, errors.New(jsonRet["errMsg"].(string)) - } - - return true, nil -} - -func (g *Group) DelMember(groupId string, userId ...string) (bool, error) { - accessToken, err := g.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return false, err - } - - bodyJson, err := json.Marshal(map[string]interface{}{ - "id": groupId, - "userList": userId, - }) - if err != nil { - return false, err - } - - encrypt, err := g.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return false, err - } - - resp, err := g.config.GetHttp().Post(groupDelMemberUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": g.config.AppId, - "buin": g.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return false, err - } - - if !resp.IsSuccess() { - return false, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return false, err - } - - if jsonRet["errcode"].(float64) != 0 { - return false, errors.New(jsonRet["errMsg"].(string)) - } - - return true, nil -} - -func (g *Group) IsMember(groupId, userId string) (bool, error) { - accessToken, err := g.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return false, err - } - - resp, err := g.config.GetHttp().Get(groupIsMemberUrl+"?accessToken="+accessToken, map[string]string{ - "id": groupId, - "userId": userId, - }) - - if err != nil { - return false, err - } - - if !resp.IsSuccess() { - return false, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return false, err - } - - if jsonRet["errcode"] != 0 { - return false, errors.New(jsonRet["errmsg"].(string)) - } - - decrypt, err := g.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return false, err - } - - var v map[string]bool - if err := decrypt.Unmarshal(&v); err != nil { - return false, err - } - - return v["belong"], nil -} diff --git a/http.go b/http.go deleted file mode 100644 index 1b1a807..0000000 --- a/http.go +++ /dev/null @@ -1,53 +0,0 @@ -package youdu - -import ( - "github.com/go-resty/resty/v2" -) - -type Http struct { - config *Config - client *resty.Client -} - -func NewHttp(config *Config) *Http { - return &Http{ - client: resty.New(), - config: config, - } -} - -func (h *Http) Request(method, url string, params interface{}, fn ...func(*resty.Request)) (*Response, error) { - var ( - req *resty.Request - resp *resty.Response - err error - ) - - req = h.client.R() - - if len(fn) > 0 { - for _, f := range fn { - f(req) - } - } - - if method == "POST" { - resp, err = req.SetBody(params).Post(h.config.Api + url) - } else { - resp, err = req.SetQueryParams(params.(map[string]string)).Get(h.config.Api + url) - } - - if err != nil { - return nil, err - } - - return NewResponse(resp), nil -} - -func (h *Http) Get(url string, params map[string]string, fn ...func(*resty.Request)) (*Response, error) { - return h.Request("GET", url, params, fn...) -} - -func (h *Http) Post(url string, params interface{}, fn ...func(*resty.Request)) (*Response, error) { - return h.Request("POST", url, params, fn...) -} diff --git a/internal/aes/aes.go b/internal/aes/aes.go new file mode 100644 index 0000000..39ceab6 --- /dev/null +++ b/internal/aes/aes.go @@ -0,0 +1,138 @@ +package aes + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "errors" + "io" +) + +const PADDING = 32 + +type RawMsg struct { + Data []byte + Length int32 + AppId string +} + +func Padding(in []byte) []byte { + padding := PADDING - (len(in) % PADDING) + if padding == 0 { + padding = PADDING + } + for i := 0; i < padding; i++ { + in = append(in, byte(padding)) + } + return in +} + +func Unpadding(in []byte) []byte { + if len(in) == 0 { + return nil + } + + padding := in[len(in)-1] + if int(padding) > len(in) || padding > PADDING { + return nil + } else if padding == 0 { + return nil + } + + for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { + if in[i] != padding { + return nil + } + } + return in[:len(in)-int(padding)] +} + +func AesEncrypt(text []byte, key []byte, appId string) (string, error) { + all := make([]byte, 0) + randBs := make([]byte, 16) + io.ReadFull(rand.Reader, randBs) + + lenBs := make([]byte, 4) + binary.BigEndian.PutUint32(lenBs, uint32(len(text))) + + all = append(all, randBs...) // 16 rand bytes + all = append(all, lenBs...) // 4 length bytes + all = append(all, []byte(text)...) // msg content + all = append(all, []byte(appId)...) // appId content + + enBs, err := aesEncrypt(all, key) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(enBs), nil +} + +func aesEncrypt(data []byte, key []byte) ([]byte, error) { + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + iv := make([]byte, 16) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + + data = Padding(data) + blockMode := cipher.NewCBCEncrypter(block, iv) + enBs := make([]byte, len(data)) + blockMode.CryptBlocks(enBs, data) + return enBs, nil +} + +func AesDecrypt(encText string, key []byte) (*RawMsg, error) { + cipherData, err := base64.StdEncoding.DecodeString(encText) + if err != nil { + return nil, err + } + + rawData, err := aesDecrypt(cipherData, key) + if err != nil { + return nil, err + } + if len(rawData) <= 20 { + return nil, errors.New("Error content length") + } + + m := &RawMsg{} + binary.Read(bytes.NewBuffer(rawData[16:20]), binary.BigEndian, &m.Length) + m.Data = rawData[20 : 20+m.Length] + m.AppId = string(rawData[20+m.Length:]) + if len(rawData) < int(20+m.Length) { + return nil, errors.New("Error length content") + } + return m, nil +} + +func aesDecrypt(data []byte, key []byte) ([]byte, error) { + keyLen := len(key) + if len(data)%keyLen != 0 { + return nil, errors.New("ciphertext size is not multiple of aes key length") + } + + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + + iv := make([]byte, aes.BlockSize) + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + return nil, err + } + blockMode := cipher.NewCBCDecrypter(block, iv) + rawData := make([]byte, len(data)) + blockMode.CryptBlocks(rawData, data) + rawData = Unpadding(rawData) + if rawData == nil { + return nil, errors.New("unpadding failed") + } + return rawData, nil +} diff --git a/media.go b/media.go deleted file mode 100644 index 0c99af9..0000000 --- a/media.go +++ /dev/null @@ -1,243 +0,0 @@ -package youdu - -import ( - "encoding/json" - "errors" - "github.com/go-resty/resty/v2" - "os" - "strconv" - "time" -) - -const ( - mediaUploadUrl = "/cgi/media/upload" - mediaGetUrl = "/cgi/media/get" - mediaSearchUrl = "/cgi/media/search" -) - -const ( - MediaTypeImage = "image" - MediaTypeFile = "file" - MediaTypeVoice = "voice" - MediaTypeVideo = "video" -) - -type Media struct { - config *Config -} - -func NewMedia(config *Config) *Media { - return &Media{ - config: config, - } -} - -func (m *Media) Upload(fileType string, filePath string) (string, error) { - accessToken, err := m.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return "", err - } - - // encrypt - fileInfo, err := os.Stat(filePath) - if err != nil { - return "", err - } - - fileByte, err := json.Marshal(map[string]interface{}{ - "type": fileType, - "name": fileInfo.Name(), - }) - if err != nil { - return "", err - } - encrypt, err := m.config.GetEncryptor().Encrypt(string(fileByte)) - if err != nil { - return "", err - } - - // 加密文件 - contentByte, err := os.ReadFile(filePath) - if err != nil { - return "", err - } - fileEncrypt, err := m.config.GetEncryptor().Encrypt(string(contentByte)) - if err != nil { - return "", err - } - tmpFile := m.config.GetPath() + "/youdu-" + fileInfo.Name() + time.Now().Format("20060102150405") + ".tmp" - defer os.Remove(tmpFile) - if err := os.WriteFile(tmpFile, []byte(fileEncrypt), 0644); err != nil { - return "", err - } - - resp, err := m.config.GetHttp().Post( - mediaUploadUrl+"?accessToken="+accessToken, - map[string]interface{}{}, - func(request *resty.Request) { - request.SetHeader("Content-Type", "multipart/form-data") - request.SetFormData(map[string]string{ - "appId": m.config.AppId, - "buin": strconv.Itoa(m.config.Buin), - "encrypt": encrypt, - }) - request.SetFile("file", tmpFile) - }) - - if err != nil { - return "", err - } - - if !resp.IsSuccess() { - return "", errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return "", err - } - - decrypt, err := m.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return "", err - } - - var v map[string]string - if err := decrypt.Unmarshal(&v); err != nil { - return "", err - } - - return v["mediaId"], err -} - -type MediaGetResp struct { - Name string `json:"name"` - Size int `json:"size"` - Body string `json:"body"` -} - -func (m *Media) Get(mediaId string) (r MediaGetResp, err error) { - accessToken, err := m.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return - } - - bodyJson, err := json.Marshal(map[string]string{ - "mediaId": mediaId, - }) - if err != nil { - return - } - - encrypt, err := m.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return - } - - resp, err := m.config.GetHttp().Post(mediaGetUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": m.config.AppId, - "buin": m.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return - } - - if !resp.IsSuccess() { - err = errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - return - } - - // decrypt body - decryptBody, err := m.config.GetEncryptor().Decrypt(resp.String()) - if err != nil { - return - } - - // decript header - encryptHeader, ok := resp.Header()["Encrypt"] - if !ok { - err = errors.New("encrypt not found") - return - } - - decryptHeader, err := m.config.GetEncryptor().Decrypt(encryptHeader[0]) - if err != nil { - return - } - - var v MediaGetResp - if err = decryptHeader.Unmarshal(&v); err != nil { - return - } - - r.Name = v.Name - r.Size = v.Size - r.Body = decryptBody.Data - - err = nil - - return -} - -type MediaInfo struct { - Name string `json:"name"` - Size int `json:"size"` -} - -func (m *Media) Search(mediaId string) (i MediaInfo, err error) { - accessToken, err := m.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return - } - - bodyJson, err := json.Marshal(map[string]string{ - "mediaId": mediaId, - }) - if err != nil { - return - } - - encrypt, err := m.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return - } - - resp, err := m.config.GetHttp().Post(mediaSearchUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": m.config.AppId, - "buin": m.config.Buin, - "encrypt": encrypt, - }) - - if err != nil { - return - } - - if !resp.IsSuccess() { - err = errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - return - } - - jsonRet, err := resp.Json() - if err != nil { - return - } - - if jsonRet["errcode"].(float64) != 0 { - err = errors.New(jsonRet["errmsg"].(string)) - return - } - - decrypt, err := m.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return - } - - var v MediaInfo - if err = decrypt.Unmarshal(&v); err != nil { - return - } - - return v, nil -} diff --git a/message.go b/message.go index ee2115d..1664293 100644 --- a/message.go +++ b/message.go @@ -1,149 +1,158 @@ package youdu import ( - "encoding/json" - "errors" - "strconv" - - "github.com/addcnos/youdu/message" + "context" + "net/http" ) +type MsgType string + const ( - msgSendUrl = "/cgi/msg/send" - popWindowUrl = "/cgi/popwindow" + MsgTypeText MsgType = "text" + MsgTypeImage MsgType = "image" + MsgTypeFile MsgType = "file" + MsgTypeMpNews MsgType = "mpnews" + MsgTypeLink MsgType = "link" + MsgTypeExLink MsgType = "exlink" ) -type Message struct { - config *Config +type MessageText struct { + Content string `json:"content"` } -func NewMessage(config *Config) *Message { - return &Message{ - config: config, - } +type MessageMedia struct { + MediaId string `json:"media_id"` } -func (m *Message) Send(msg message.Message) error { - accessToken, err := m.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return err - } - - msgJson, err := json.Marshal(msg) - if err != nil { - return err - } - - encrypt, err := m.config.GetEncryptor().Encrypt(string(msgJson)) - if err != nil { - return err - } +type MessageMpNews struct { + Title string `json:"title"` + MediaId string `json:"media_id"` + Content string `json:"content"` + Digest string `json:"digest,omitempty"` + ShowFront int `json:"showFront,omitempty"` +} - resp, err := m.config.GetHttp().Post(msgSendUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": m.config.AppId, - "buin": m.config.Buin, - "encrypt": encrypt, - }) - if err != nil { - return err - } +type MessageLink struct { + Title string `json:"title"` + Url string `json:"url"` + Action int `json:"action,omitempty"` +} - if !resp.IsSuccess() { - return errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } +type MessageExLink struct { + Title string `json:"title"` + Url string `json:"url"` + MediaId string `json:"media_id"` + Digest string `json:"digest,omitempty"` +} - jsonRet, err := resp.Json() - if err != nil { - return err - } +type InterfaceMessageRequest interface{} - if jsonRet["errcode"].(float64) != 0 { - return errors.New(jsonRet["errmsg"].(string)) - } +var ( + _ InterfaceMessageRequest = MessageRequest{} + _ InterfaceMessageRequest = TextMessageRequest{} + _ InterfaceMessageRequest = ImageMessageRequest{} + _ InterfaceMessageRequest = FileMessageRequest{} + _ InterfaceMessageRequest = MpNewsMessageRequest{} + _ InterfaceMessageRequest = LinkMessageRequest{} + _ InterfaceMessageRequest = ExLinkMessageRequest{} +) - return nil +type MessageRequest struct { + ToUser string `json:"toUser"` + ToDept string `json:"toDept"` + MsgType MsgType `json:"msgType"` + Text MessageText `json:"text,omitempty"` + Image MessageMedia `json:"image,omitempty"` + File MessageMedia `json:"file,omitempty"` + MpNews []MessageMpNews `json:"mpnews,omitempty"` + Link MessageLink `json:"link,omitempty"` + ExLink []MessageExLink `json:"exlink,omitempty"` } -func (m *Message) SendText(toUser, content string, toDept ...string) error { - if len(toDept) == 0 { - toDept = []string{""} - } +type TextMessageRequest struct { + ToUser string `json:"toUser"` + ToDept string `json:"toDept"` + MsgType MsgType `json:"msgType"` + Text MessageText `json:"text"` +} - return m.Send(&message.TextMessage{ - ToUser: toUser, - ToDept: toDept[0], - MsgType: message.MsgTypeText, - Text: &message.TextItem{ - Content: content, - }, - }) +type ImageMessageRequest struct { + ToUser string `json:"toUser"` + ToDept string `json:"toDept"` + MsgType MsgType `json:"msgType"` + Image MessageMedia `json:"image"` } -func (m *Message) SendImage(toUser, mediaId string, toDept ...string) error { - if len(toDept) == 0 { - toDept = []string{""} - } +type FileMessageRequest struct { + ToUser string `json:"toUser"` + ToDept string `json:"toDept"` + MsgType MsgType `json:"msgType"` + File MessageMedia `json:"file"` +} - return m.Send(&message.ImageMessage{ - ToUser: toUser, - ToDept: toDept[0], - MsgType: message.MsgTypeImage, - Image: &message.MediaItem{ - MediaId: mediaId, - }, - }) +type MpNewsMessageRequest struct { + ToUser string `json:"toUser"` + ToDept string `json:"toDept"` + MsgType MsgType `json:"msgType"` + MpNews []MessageMpNews `json:"mpnews"` } -func (m *Message) SendFile(toUser, mediaId string, toDept ...string) error { - if len(toDept) == 0 { - toDept = []string{""} - } +type LinkMessageRequest struct { + ToUser string `json:"toUser"` + ToDept string `json:"toDept"` + MsgType MsgType `json:"msgType"` + Link MessageLink `json:"link"` +} - return m.Send(&message.FileMessage{ - ToUser: toUser, - ToDept: toDept[0], - MsgType: message.MsgTypeFile, - File: &message.MediaItem{ - MediaId: mediaId, - }, - }) +type ExLinkMessageRequest struct { + ToUser string `json:"toUser"` + ToDept string `json:"toDept"` + MsgType MsgType `json:"msgType"` + ExLink []MessageExLink `json:"exlink"` } -func (m *Message) Popwindow(msg message.Message) error { - if _, ok := msg.(*message.PopWindowMessage); !ok { - return errors.New("message must be PopWindowMessage") - } +type MessageResponse struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} - msgJson, err := json.Marshal(msg) +func (c *Client) SendMessage(ctx context.Context, request InterfaceMessageRequest) (response Response, err error) { + req, err := c.newRequest(ctx, http.MethodPost, "/cgi/msg/send", + withRequestBody(request), withRequestAccessToken(), withRequestEncrypt()) if err != nil { - return err + return } - encrypt, err := m.config.GetEncryptor().Encrypt(string(msgJson)) - if err != nil { - return err - } + err = c.sendRequest(req, &response) + return +} - resp, err := m.config.GetHttp().Post(popWindowUrl, map[string]interface{}{ - "app_id": m.config.AppId, - "msg_encrypt": encrypt, - }) - if err != nil { - return err - } +func (c *Client) SendTextMessage(ctx context.Context, request TextMessageRequest) (response Response, err error) { + request.MsgType = MsgTypeText + return c.SendMessage(ctx, request) +} - if !resp.IsSuccess() { - return errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } +func (c *Client) SendImageMessage(ctx context.Context, request ImageMessageRequest) (response Response, err error) { + request.MsgType = MsgTypeImage + return c.SendMessage(ctx, request) +} - jsonRet, err := resp.Json() - if err != nil { - return err - } +func (c *Client) SendFileMessage(ctx context.Context, request FileMessageRequest) (response Response, err error) { + request.MsgType = MsgTypeFile + return c.SendMessage(ctx, request) +} - if jsonRet["errcode"].(float64) != 0 { - return errors.New(jsonRet["errmsg"].(string)) - } +func (c *Client) SendMpNewsMessage(ctx context.Context, request MpNewsMessageRequest) (response Response, err error) { + request.MsgType = MsgTypeMpNews + return c.SendMessage(ctx, request) +} + +func (c *Client) SendLinkMessage(ctx context.Context, request LinkMessageRequest) (response Response, err error) { + request.MsgType = MsgTypeLink + return c.SendMessage(ctx, request) +} - return nil +func (c *Client) SendExLinkMessage(ctx context.Context, request ExLinkMessageRequest) (response Response, err error) { + request.MsgType = MsgTypeExLink + return c.SendMessage(ctx, request) } diff --git a/message/message.go b/message/message.go deleted file mode 100644 index 6e96742..0000000 --- a/message/message.go +++ /dev/null @@ -1,163 +0,0 @@ -package message - -// see:https://youdu.im/doc/api/c01_00003.html#_7 -const ( - MsgTypeText = "text" - MsgTypeImage = "image" - MsgTypeFile = "file" - MsgTypeMpNews = "mpnews" - MsgTypeAudio = "audio" - MsgTypeVideo = "video" - MsgTypeLink = "link" - MsgTypeExtLink = "exlink" - MsgTypeSys = "sysMsg" - MsgTypeSms = "sms" - MsgTypeMail = "mail" -) - -type Message interface{} - -var ( - _ Message = (*TextMessage)(nil) - _ Message = (*ImageMessage)(nil) - _ Message = (*FileMessage)(nil) - _ Message = (*MpNewsMessage)(nil) - _ Message = (*LinkMessage)(nil) - _ Message = (*ExLinkMessage)(nil) - _ Message = (*SmsMessage)(nil) - _ Message = (*MailMessage)(nil) -) - -type TextItem struct { - Content string `json:"content"` -} - -type TextMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - MsgType string `json:"msgType"` - Text *TextItem `json:"text"` -} - -type MediaItem struct { - MediaId string `json:"media_id"` -} - -type ImageMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - MsgType string `json:"msgType"` - Image *MediaItem `json:"image"` -} - -type FileMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - MsgType string `json:"msgType"` - File *MediaItem `json:"file"` -} - -type MpNewsItem struct { - Title string `json:"title"` - MediaId string `json:"media_id"` - Content string `json:"content"` - Digest string `json:"digest"` - ShowFront int `json:"show_front"` -} - -type MpNewsMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - MsgType string `json:"msgType"` - MpNews []*MpNewsItem `json:"mpNews"` -} - -type LinkItem struct { - Title string `json:"title"` - Url string `json:"url"` - Action int `json:"action"` -} - -type LinkMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - MsgType string `json:"msgType"` - Link *LinkItem `json:"link"` -} - -type ExLinkItem struct { - Title string `json:"title"` - Url string `json:"url"` - Digest string `json:"digest"` - MediaId string `json:"media_id"` -} - -type ExLinkMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - MsgType string `json:"msgType"` - ExLink []*ExLinkItem `json:"exlink"` -} - -type SysMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - ToAll struct { - OnlyOnline bool `json:"onlyOnline"` - } `json:"toAll"` - MsgType string `json:"msgType"` - SysMsg *SysMsgItem `json:"sysMsg"` -} - -type SysMsgItem struct { - Title string `json:"title"` - PopDuration int `json:"popDuration"` - Msg []*SysMsgItemMsg `json:"msg"` -} -type SysMsgItemMsg struct { - Text *TextItem `json:"text,omitempty"` - Link *LinkItem `json:"link,omitempty"` -} - -type SmsMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - MsgType string `json:"msgType"` - Sms *struct { - From string `json:"from"` - Content string `json:"content"` - } `json:"sms"` -} - -type MailMessage struct { - ToUser string `json:"toUser"` - ToEmail string `json:"toEmail"` - MsgType string `json:"msgType"` - Mail *struct { - Action string `json:"action"` - Subject string `json:"subject"` - FromUser string `json:"fromUser"` - FromEmail string `json:"fromEmail"` - Time int `json:"time"` - Link string `json:"link"` - UnreadCount int `json:"unreadCount"` - } -} - -type PopWindowItem struct { - Url string `json:"url"` - Tip string `json:"tip"` - Title string `json:"title"` - Width int `json:"width"` - Height int `json:"height"` - Duration int `json:"duration"` - Position int `json:"position"` - NoticeId string `json:"notice_id"` - PopMode int `json:"pop_mode"` -} - -type PopWindowMessage struct { - ToUser string `json:"toUser"` - ToDept string `json:"toDept"` - PopWindow *PopWindowItem `json:"popWindow"` -} diff --git a/options.go b/options.go new file mode 100644 index 0000000..0c759a2 --- /dev/null +++ b/options.go @@ -0,0 +1,111 @@ +package youdu + +import ( + "bytes" + "encoding/json" + "io" + "net/url" +) + +type Request struct { + Buin int `json:"buin"` + AppId string `json:"appId"` + Encrypt string `json:"encrypt"` +} + +type Response struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` + Encrypt string `json:"encrypt,omitempty"` +} + +type requestOptions struct { + params url.Values + body interface{} + needEncrypt bool + needAccessToken bool +} + +func newRequestOptions(opts ...requestOption) *requestOptions { + args := &requestOptions{ + body: nil, + params: url.Values{}, + } + + for _, opt := range opts { + opt(args) + } + + return args +} + +func (r *requestOptions) bodyReader(body interface{}) (io.Reader, error) { + if body == nil { + return nil, nil + } + + if v, ok := body.(io.Reader); ok { + return v, nil + } + + reqBytes, err := json.Marshal(body) + if err != nil { + return nil, err + } + + return bytes.NewBuffer(reqBytes), nil +} + +type requestOption func(*requestOptions) + +func withRequestBody(body interface{}) requestOption { + return func(args *requestOptions) { + args.body = body + } +} + +func withRequestEncrypt() requestOption { + return func(args *requestOptions) { + args.needEncrypt = true + } +} + +func withRequestParams(params url.Values) requestOption { + return func(args *requestOptions) { + args.params = params + } +} + +func withRequestParamsKV(key, value string) requestOption { + return func(args *requestOptions) { + args.params.Add(key, value) + } +} + +func withRequestAccessToken() requestOption { + return func(args *requestOptions) { + args.needAccessToken = true + } +} + +type responseOptions struct { + needDecrypt bool +} + +type responseOption func(*responseOptions) + +func newResponseOptions(opts ...responseOption) *responseOptions { + args := &responseOptions{} + + for _, opt := range opts { + opt(args) + } + + return args +} + +func withResponseDecrypt() responseOption { + return func(args *responseOptions) { + args.needDecrypt = true + } +} diff --git a/response.go b/response.go deleted file mode 100644 index eee2dcc..0000000 --- a/response.go +++ /dev/null @@ -1,45 +0,0 @@ -package youdu - -import ( - "encoding/json" - "github.com/go-resty/resty/v2" -) - -type Response struct { - restyResponse *resty.Response -} - -func NewResponse(restyResponse *resty.Response) *Response { - return &Response{ - restyResponse: restyResponse, - } -} - -func (r *Response) Body() []byte { - return r.restyResponse.Body() -} - -func (r *Response) String() string { - return r.restyResponse.String() -} - -func (r *Response) Json() (map[string]interface{}, error) { - var v map[string]interface{} - if err := json.Unmarshal(r.restyResponse.Body(), &v); err != nil { - return nil, err - } - - return v, nil -} - -func (r *Response) StatusCode() int { - return r.restyResponse.StatusCode() -} - -func (r *Response) Header() map[string][]string { - return r.restyResponse.Header() -} - -func (r *Response) IsSuccess() bool { - return r.StatusCode() == 200 -} diff --git a/session.go b/session.go deleted file mode 100644 index eecef3a..0000000 --- a/session.go +++ /dev/null @@ -1,219 +0,0 @@ -package youdu - -import ( - "encoding/json" - "errors" - "fmt" - "strconv" - - "github.com/addcnos/youdu/session" -) - -const ( - sessionCreateUrl = "/cgi/session/create" - sessionGetUrl = "/cgi/session/get" - sessionUpdateUrl = "/cgi/session/update" - sessionSendUrl = "/cgi/session/send" -) - -type Session struct { - config *Config -} - -func NewSession(config *Config) *Session { - return &Session{ - config: config, - } -} - -// Create 创建一个会话 -// members 第一个默认为创建者 -func (s *Session) Create(title string, members []string) (*session.Session, error) { - if len(members) < 3 { - return nil, errors.New("members must be at least 3") - } - - accessToken, err := s.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return nil, err - } - - bodyJson, err := json.Marshal(map[string]interface{}{ - "title": title, - "creator": members[0], - "member": members, - "type": "multi", - }) - if err != nil { - return nil, err - } - - encrypt, err := s.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return nil, err - } - - resp, err := s.config.GetHttp().Post(sessionCreateUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": s.config.AppId, - "buin": s.config.Buin, - "encrypt": encrypt, - }) - if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return nil, err - } - - decrypt, err := s.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return nil, err - } - - var v *session.Session - if err := decrypt.Unmarshal(&v); err != nil { - return nil, err - } - - return v, nil -} - -// Get 获取会话信息 -func (s *Session) Get(sessionId string) (*session.Session, error) { - accessToken, err := s.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return nil, err - } - - resp, err := s.config.GetHttp().Get(sessionGetUrl+"?accessToken="+accessToken, map[string]string{ - "sessionId": sessionId, - }) - if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return nil, err - } - - decrypt, err := s.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return nil, err - } - - var v *session.Session - if err := decrypt.Unmarshal(&v); err != nil { - return nil, err - } - - return v, nil -} - -// Update 更新会话信息 -func (s *Session) Update(sessionId, opUser, title string, addMembers, delMembers []string) (*session.Session, error) { - accessToken, err := s.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return nil, err - } - - bodyJson, err := json.Marshal(map[string]interface{}{ - "sessionId": sessionId, - "opUser": opUser, - "title": title, - "addMember": addMembers, - "delMember": delMembers, - }) - if err != nil { - return nil, err - } - - encrypt, err := s.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return nil, err - } - - resp, err := s.config.GetHttp().Post(sessionUpdateUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": s.config.AppId, - "buin": s.config.Buin, - "encrypt": encrypt, - }) - if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return nil, err - } - - decrypt, err := s.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return nil, err - } - - fmt.Println(decrypt) - - var v *session.Session - if err := decrypt.Unmarshal(&v); err != nil { - return nil, err - } - - return v, nil -} - -// Send 发送消息 -func (s *Session) Send(message session.Message) error { - accessToken, err := s.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return err - } - - bodyJson, err := json.Marshal(message) - if err != nil { - return err - } - - encrypt, err := s.config.GetEncryptor().Encrypt(string(bodyJson)) - if err != nil { - return err - } - - resp, err := s.config.GetHttp().Post(sessionSendUrl+"?accessToken="+accessToken, map[string]interface{}{ - "appId": s.config.AppId, - "buin": s.config.Buin, - "encrypt": encrypt, - }) - if err != nil { - return err - } - - if !resp.IsSuccess() { - return errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return err - } - - if jsonRet["errcode"].(float64) != 0 { - return errors.New(jsonRet["errmsg"].(string)) - } - - return nil -} diff --git a/session/message.go b/session/message.go deleted file mode 100644 index 21a9e51..0000000 --- a/session/message.go +++ /dev/null @@ -1,65 +0,0 @@ -package session - -type Message interface{} - -var ( - _ Message = (*TextMessage)(nil) - _ Message = (*ImageMessage)(nil) - _ Message = (*VoiceMessage)(nil) - _ Message = (*VideoMessage)(nil) -) - -const ( - MsgTypeText = "text" - MsgTypeImage = "image" - MsgTypeVoice = "voice" - MsgTypeVideo = "video" -) - -type TextItem struct { - Content string `json:"content"` -} - -type TextMessage struct { - SessionId string `json:"sessionId,omitempty"` - Receiver string `json:"receiver,omitempty"` - Sender string `json:"sender"` - MsgType string `json:"msgType"` - Text *TextItem `json:"text"` -} - -type MediaItem struct { - MediaId string `json:"media_id"` -} - -type ImageMessage struct { - SessionId string `json:"sessionId,omitempty"` - Receiver string `json:"receiver,omitempty"` - Sender string `json:"sender"` - MsgType string `json:"msgType"` - Image *MediaItem `json:"image"` -} - -type FileMessage struct { - SessionId string `json:"sessionId,omitempty"` - Receiver string `json:"receiver,omitempty"` - Sender string `json:"sender"` - MsgType string `json:"msgType"` - File *MediaItem `json:"file"` -} - -type VoiceMessage struct { - SessionId string `json:"sessionId,omitempty"` - Receiver string `json:"receiver,omitempty"` - Sender string `json:"sender"` - MsgType string `json:"msgType"` - Voice *MediaItem `json:"voice"` -} - -type VideoMessage struct { - SessionId string `json:"sessionId,omitempty"` - Receiver string `json:"receiver,omitempty"` - Sender string `json:"sender"` - MsgType string `json:"msgType"` - Video *MediaItem `json:"video"` -} diff --git a/session/session.go b/session/session.go deleted file mode 100644 index b3a8139..0000000 --- a/session/session.go +++ /dev/null @@ -1,12 +0,0 @@ -package session - -type Session struct { - SessionId string `json:"sessionId"` - Type string `json:"type"` - Owner string `json:"owner"` - Title string `json:"title"` - Version int `json:"version"` - Member []string `json:"member"` - LastMsgId int `json:"lastMsgId"` - ActiveTime int `json:"activeTime"` -} diff --git a/store.go b/store.go new file mode 100644 index 0000000..b030d11 --- /dev/null +++ b/store.go @@ -0,0 +1,5 @@ +package youdu + +type Store interface { + Put() +} diff --git a/token.go b/token.go new file mode 100644 index 0000000..2045ce0 --- /dev/null +++ b/token.go @@ -0,0 +1,60 @@ +package youdu + +import ( + "context" + "strconv" + "time" +) + +type token struct { + Token string + Expired time.Time +} + +func (c *Client) GetToken(ctx context.Context) (string, error) { + if c.token != nil && c.token.Expired.After(time.Now()) { + return c.token.Token, nil + } + + ciphertext, err := c.encryptor.Encrypt([]byte(strconv.Itoa(int(time.Now().Unix())))) + if err != nil { + return "", err + } + + resp, err := c.getToken(ctx, tokenRequest{ + Buin: c.config.Buin, + AppId: c.config.AppId, + Encrypt: ciphertext, + }) + if err != nil { + return "", err + } + + c.token = &token{ + Token: resp.AccessToken, + Expired: time.Now().Add(time.Duration(resp.ExpireIn)*time.Second - 10*time.Minute), // 提前10分钟过期 + } + + return resp.AccessToken, nil +} + +type tokenRequest struct { + Buin int `json:"buin"` + AppId string `json:"appId"` + Encrypt string `json:"encrypt"` +} + +type tokenResponse struct { + AccessToken string `json:"accessToken"` + ExpireIn int `json:"expireIn"` +} + +func (c *Client) getToken(ctx context.Context, request tokenRequest) (response tokenResponse, err error) { + req, err := c.newRequest(ctx, "POST", "/cgi/gettoken", withRequestBody(request)) + if err != nil { + return + } + + err = c.sendRequest(req, &response, withResponseDecrypt()) + return +} diff --git a/user.go b/user.go index 1fd13e5..f2fdf57 100644 --- a/user.go +++ b/user.go @@ -1,203 +1,68 @@ package youdu import ( - "errors" + "context" + "net/http" "strconv" ) -const ( - userGetUrl = "/cgi/user/get" - userListUrl = "/cgi/user/list" - userSimpleListUrl = "/cgi/user/simplelist" - userEnableStateUrl = "/cgi/user/enable/state" -) - -type User struct { - config *Config +type DeptDetail struct { + DeptId int `json:"deptId"` + Position string `json:"position"` + Weight int `json:"weight"` + SortId int `json:"sortId"` } -func NewUser(config *Config) *User { - return &User{ - config: config, - } +type UserResponse struct { + UserId string `json:"userId"` + Name string `json:"name"` + Gender int `json:"gender"` + Mobile string `json:"mobile"` + Phone string `json:"phone"` + Email string `json:"email"` + Dept []int `json:"dept"` + DeptDetail []DeptDetail `json:"deptDetail"` } -type UserInfo struct { - Gid int `json:"gid"` - UserId string `json:"userId"` - Name string `json:"name"` - Gender int `json:"gender"` // 性别。0表示男性,1表示女性 - Mobile string `json:"mobile"` - Phone string `json:"phone"` - Email string `json:"email"` - Dept []int `json:"dept"` - DeptDetail []struct { - DeptId int `json:"deptId"` - DeptName string `json:"deptName"` - Position string `json:"position"` - Weight int `json:"weight"` - SortId int `json:"sortId"` - } `json:"deptDetail"` - Attrs []interface{} `json:"attrs"` +type UserList struct { + UserId string `json:"userId"` + Name string `json:"name"` + Gender interface{} `json:"gender"` + Mobile string `json:"mobile"` + Phone string `json:"phone"` + Email string `json:"email"` + Dept []int `json:"dept"` + DeptDetail []DeptDetail `json:"deptDetail"` } -// Get 获取用户信息 -// see: https://youdu.im/doc/api/c01_00013.html#_6 -func (u *User) Get(userId string) (UserInfo, error) { - accessToken, err := u.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return UserInfo{}, err - } - - resp, err := u.config.GetHttp().Get(userGetUrl, map[string]string{ - "userId": userId, - "accessToken": accessToken, - }) - - if err != nil { - return UserInfo{}, err - } - - if !resp.IsSuccess() { - return UserInfo{}, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return UserInfo{}, err - } - - decrypt, err := u.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return UserInfo{}, err - } - - var v UserInfo - if err := decrypt.Unmarshal(&v); err != nil { - return UserInfo{}, err - } - - return v, nil +type DeptUserListResponse struct { + UserList []UserList `json:"userList"` } -// List 获取部门用户详细信息 -func (u *User) List(deptId int) ([]UserInfo, error) { - accessToken, err := u.config.GetAccessTokenProvider().GetAccessToken() +func (c *Client) GetUser(ctx context.Context, userId string) (response UserResponse, err error) { + req, err := c.newRequest(ctx, http.MethodGet, "/cgi/user/get", + withRequestAccessToken(), + withRequestEncrypt(), + withRequestParamsKV("userId", userId), + ) if err != nil { - return nil, err + return } - resp, err := u.config.GetHttp().Get(userListUrl, map[string]string{ - "deptId": strconv.Itoa(deptId), - "accessToken": accessToken, - }) - - if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return nil, err - } - - decrypt, err := u.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return nil, err - } - - var v map[string][]UserInfo - if err := decrypt.Unmarshal(&v); err != nil { - return nil, err - } - - return v["userList"], nil + err = c.sendRequest(req, &response, withResponseDecrypt()) + return } -type SimpleUserInfo struct { - Gid int `json:"gid"` - UserId string `json:"userId"` - Name string `json:"name"` - Gender int `json:"gender"` - Dept []int `json:"dept"` -} - -// SimpleList 获取部门用户 -func (u *User) SimpleList(deptId int) ([]SimpleUserInfo, error) { - accessToken, err := u.config.GetAccessTokenProvider().GetAccessToken() - if err != nil { - return nil, err - } - - resp, err := u.config.GetHttp().Get(userSimpleListUrl, map[string]string{ - "deptId": strconv.Itoa(deptId), - "accessToken": accessToken, - }) - - if err != nil { - return nil, err - } - - if !resp.IsSuccess() { - return nil, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return nil, err - } - - decrypt, err := u.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return nil, err - } - - var v map[string][]SimpleUserInfo - if err := decrypt.Unmarshal(&v); err != nil { - return nil, err - } - - return v["userList"], nil -} - -// EnableState 查询用户激活状态 -func (u *User) EnableState(userId string) (int, error) { - accessToken, err := u.config.GetAccessTokenProvider().GetAccessToken() +func (c *Client) GetDeptUserList(ctx context.Context, deptId int) (response DeptUserListResponse, err error) { + req, err := c.newRequest(ctx, http.MethodGet, "/cgi/user/list", + withRequestAccessToken(), + withRequestEncrypt(), + withRequestParamsKV("deptId", strconv.Itoa(deptId)), + ) if err != nil { - return 0, err - } - - resp, err := u.config.GetHttp().Get(userEnableStateUrl, map[string]string{ - "userId": userId, - "accessToken": accessToken, - }) - - if err != nil { - return 0, err - } - - if !resp.IsSuccess() { - return 0, errors.New("Response status code is " + strconv.Itoa(resp.StatusCode())) - } - - jsonRet, err := resp.Json() - if err != nil { - return 0, err - } - - decrypt, err := u.config.GetEncryptor().Decrypt(jsonRet["encrypt"].(string)) - if err != nil { - return 0, err - } - - var v map[string]int - if err := decrypt.Unmarshal(&v); err != nil { - return 0, err + return } - return v["enableState"], nil + err = c.sendRequest(req, &response, withResponseDecrypt()) + return } diff --git a/youdu.go b/youdu.go deleted file mode 100644 index cbf6748..0000000 --- a/youdu.go +++ /dev/null @@ -1,97 +0,0 @@ -package youdu - -type Youdu struct { - config *Config - - dept *Dept - message *Message - media *Media - user *User - session *Session - group *Group - auth *Auth -} - -// New 创建一个 Youdu 实例 -func New(config *Config) *Youdu { - return &Youdu{ - config: config, - } -} - -// Message 创建消息相关的实例 -func (y *Youdu) Message() *Message { - if y.message == nil { - y.message = NewMessage(y.config) - } - - return y.message -} - -// Media 创建媒体相关的实例 -func (y *Youdu) Media() *Media { - if y.media == nil { - y.media = NewMedia(y.config) - } - - return y.media -} - -// Dept 创建部门相关的实例 -func (y *Youdu) Dept() *Dept { - if y.dept == nil { - y.dept = NewDept(y.config) - } - - return y.dept -} - -// User 创建用户相关的实例 -func (y *Youdu) User() *User { - if y.user == nil { - y.user = NewUser(y.config) - } - - return y.user -} - -// Session 创建会话相关的实例 -func (y *Youdu) Session() *Session { - if y.session == nil { - y.session = NewSession(y.config) - } - - return y.session -} - -// Group 创建群相关的实例 -func (y *Youdu) Group() *Group { - if y.group == nil { - y.group = NewGroup(y.config) - } - - return y.group -} - -func (y *Youdu) Auth() *Auth { - if y.auth == nil { - y.auth = NewAuth(y.config) - } - - return y.auth -} - -// AccessToken 返回 accessToken -func (y *Youdu) AccessToken() (string, error) { - return y.config.GetAccessTokenProvider().GetAccessToken() -} - -// Encryptor 返回加密器 -func (y *Youdu) Encryptor() *encryptor { - return y.config.GetEncryptor() -} - -// Config 获取配置 -func (y *Youdu) Config() *Config { - return y.config -}