| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216 |
- // Copyright 2019 Yunion
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package feishu
- import (
- "context"
- "fmt"
- "net/http"
- "time"
- "yunion.io/x/jsonutils"
- "yunion.io/x/pkg/errors"
- "yunion.io/x/pkg/util/httputils"
- )
- const (
- // 获取 tenant_access_token(企业自建应用)
- ApiTenantAccessTokenInternal = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal/"
- // 获取群列表
- ApiChatList = "https://open.feishu.cn/open-apis/chat/v4/list"
- // 机器人发送消息
- ApiRobotSendMessage = "https://open.feishu.cn/open-apis/message/v4/send/"
- // 批量发送消息
- ApiRobotBatchSendMessage = "https://open.feishu.cn/open-apis/message/v4/batch_send/"
- // 使用手机号或邮箱获取用户ID
- ApiFetchUserID = "https://open.feishu.cn/open-apis/user/v1/batch_get_id"
- // 使用 webhook 机器人发送消息
- ApiWebhookRobotSendMessage = "https://open.feishu.cn/open-apis/bot/hook/"
- )
- var (
- cli = &http.Client{
- Transport: httputils.GetTransport(true),
- }
- ctx = context.Background()
- )
- func Request(method httputils.THttpMethod, url string, header http.Header, body jsonutils.JSONObject) (jsonutils.JSONObject, error) {
- _, resp, err := httputils.JSONRequest(cli, ctx, method, url, header, body, false)
- return resp, err
- }
- func checkErr(resp CommonResponser) error {
- if resp.GetCode() != 0 {
- return errors.Error(fmt.Sprintf("response error, code: %d, msg: %s", resp.GetCode(), resp.GetMsg()))
- }
- return nil
- }
- func unmarshal(resp jsonutils.JSONObject, obj CommonResponser) error {
- if err := resp.Unmarshal(obj); err != nil {
- return errors.Wrap(err, "unmarshal json")
- }
- return checkErr(obj)
- }
- // 获取 tenant_access_token(企业自建应用)https://open.feishu.cn/document/ukTMukTMukTM/uIjNz4iM2MjLyYzM
- func GetTenantAccessTokenInternal(appId string, appSecret string) (*TenantAccesstokenResp, error) {
- body := jsonutils.NewDict()
- body.Add(jsonutils.NewString(appId), "app_id")
- body.Add(jsonutils.NewString(appSecret), "app_secret")
- ret, err := Request(httputils.POST, ApiTenantAccessTokenInternal, http.Header{}, body)
- if err != nil {
- return nil, err
- }
- obj := new(TenantAccesstokenResp)
- err = unmarshal(ret, obj)
- return obj, err
- }
- type Tenant struct {
- AppId string
- AppSecret string
- AccessToken string
- Cache ICache
- }
- func BuildTokenHeader(token string) http.Header {
- h := http.Header{}
- h.Add("Authorization", fmt.Sprintf("Bearer %s", token))
- return h
- }
- func NewTenant(appId, appSecret string) (*Tenant, error) {
- t := &Tenant{
- AppId: appId,
- AppSecret: appSecret,
- AccessToken: "",
- Cache: nil,
- }
- t.Cache = NewFileCache(fmt.Sprintf(".%s_auth_file", appId))
- err := t.RefreshAccessToken()
- return t, err
- }
- // RefreshAccessToken is to get a valid access token
- func (t *Tenant) RefreshAccessToken() error {
- var data TenantAccesstoken
- err := t.Cache.Get(&data)
- if err == nil {
- t.AccessToken = data.TenantAccessToken
- return nil
- }
- tokenResp, err := GetTenantAccessTokenInternal(t.AppId, t.AppSecret)
- if err == nil {
- t.AccessToken = tokenResp.TenantAccessToken
- data = tokenResp.TenantAccesstoken
- data.Created = time.Now().Unix()
- err = t.Cache.Set(&data)
- }
- return err
- }
- func (t *Tenant) request(method httputils.THttpMethod, url string, data jsonutils.JSONObject, out CommonResponser) error {
- obj, err := Request(method, url, BuildTokenHeader(t.AccessToken), data)
- if err != nil {
- return err
- }
- err = unmarshal(obj, out)
- return err
- }
- func (t *Tenant) get(url string, query jsonutils.JSONObject, out CommonResponser) error {
- if query != nil {
- qs := query.QueryString()
- if len(qs) > 0 {
- url = fmt.Sprintf("%s?%s", url, qs)
- }
- }
- return t.request(httputils.GET, url, nil, out)
- }
- func (t *Tenant) post(url string, body jsonutils.JSONObject, out CommonResponser) error {
- return t.request(httputils.POST, url, body, out)
- }
- func (t *Tenant) ChatList(pageSize int, pageToken string) (*GroupListResp, error) {
- query := jsonutils.NewDict()
- if pageSize > 0 {
- query.Add(jsonutils.NewInt(int64(pageSize)), "page_size")
- }
- if pageToken != "" {
- query.Add(jsonutils.NewString(pageToken), "page_token")
- }
- resp := new(GroupListResp)
- err := t.get(ApiChatList, query, resp)
- return resp, err
- }
- func (t *Tenant) SendMessage(msg MsgReq) (*MsgResp, error) {
- body := jsonutils.Marshal(msg)
- resp := new(MsgResp)
- err := t.post(ApiRobotSendMessage, body, resp)
- return resp, err
- }
- // UserIdByMobile query the open id of user by mobile number. https://open.feishu.cn/document/ukTMukTMukTM/uUzMyUjL1MjM14SNzITN
- func (t *Tenant) UserIdByMobile(mobile string) (string, error) {
- query := jsonutils.NewDict()
- query.Set("mobiles", jsonutils.NewString(mobile))
- resp := new(UserIDResp)
- err := t.get(ApiFetchUserID, query, resp)
- if err != nil {
- return "", err
- }
- if len(resp.Data.MobilesNotExist) != 0 {
- return "", errors.Wrapf(errors.ErrNotFound, "no such user whose mobile is %s", mobile)
- }
- list, err := resp.Data.MobileUsers.GetArray(mobile)
- if err != nil {
- return "", errors.Wrap(err, "jsonutils.JSONObject.GetArray")
- }
- // len(list) must be positive
- return list[0].GetString("open_id")
- }
- // SendWebhookRobotMessage will send to message to Webhook address.
- // Webhook's format: https://open.feishu.cn/open-apis/bot/hook/xxxxxxxxxxxxxxxxxxxxxxxxxxx.
- // The hook represents the last part of webhook: 'xxxxxxxxxxxxxxxxxxxxxxxxxxx'.
- func SendWebhookRobotMessage(hook string, msg WebhookRobotMsgReq) (*WebhookRobotMsgResp, error) {
- url := ApiWebhookRobotSendMessage + hook
- obj, err := Request(httputils.POST, url, http.Header{}, jsonutils.Marshal(msg))
- if err != nil {
- return nil, err
- }
- resp := new(WebhookRobotMsgResp)
- err = obj.Unmarshal(resp)
- if err != nil {
- return nil, errors.Wrap(err, "unmarshal json")
- }
- if !resp.Ok {
- return resp, fmt.Errorf("response error, msg: %s", resp.Error)
- }
- return resp, err
- }
- // BatchSendMessage batch send messages. Doc: https://open.feishu.cn/document/ukTMukTMukTM/ucDO1EjL3gTNx4yN4UTM
- func (t *Tenant) BatchSendMessage(msg BatchMsgReq) (*BatchMsgResp, error) {
- body := jsonutils.Marshal(msg)
- resp := new(BatchMsgResp)
- err := t.post(ApiRobotBatchSendMessage, body, resp)
- return resp, err
- }
|