feishu.go 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package sender
  15. import (
  16. "context"
  17. "fmt"
  18. "net/http"
  19. "net/url"
  20. "yunion.io/x/jsonutils"
  21. "yunion.io/x/pkg/errors"
  22. "yunion.io/x/pkg/util/httputils"
  23. api "yunion.io/x/onecloud/pkg/apis/notify"
  24. "yunion.io/x/onecloud/pkg/monitor/notifydrivers/feishu"
  25. "yunion.io/x/onecloud/pkg/notify/models"
  26. )
  27. type SFeishuSender struct {
  28. config map[string]api.SNotifyConfigContent
  29. }
  30. func (self *SFeishuSender) GetSenderType() string {
  31. return api.FEISHU
  32. }
  33. const ApiSendMessageForFeishuByOpenId = feishu.ApiRobotSendMessage + "?receive_id_type=open_id"
  34. // 发送飞书消息
  35. func (feishuSender *SFeishuSender) Send(ctx context.Context, args api.SendParams) error {
  36. // 发送通知消息体
  37. body := map[string]interface{}{
  38. "open_id": args.Receivers.Contact,
  39. "msg_type": "text",
  40. "content": map[string]interface{}{
  41. "text": args.Message,
  42. },
  43. } // 添加bearer token请求头
  44. header := http.Header{}
  45. header.Add("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[fmt.Sprintf("%s-%s", api.FEISHU, args.DomainId)].Content.AccessToken))
  46. rep, err := sendRequest(ctx, ApiSendMessageForFeishuByOpenId, httputils.POST, header, nil, jsonutils.Marshal(body))
  47. if err == nil {
  48. return nil
  49. }
  50. if rep == nil {
  51. return errors.Wrap(err, "feishu.sendRequest")
  52. }
  53. // 发送通知失败的情况
  54. code, err := rep.GetString("code")
  55. if err != nil {
  56. return err
  57. }
  58. switch code {
  59. case "99991663": //token过期
  60. err = feishuSender.GetAccessToken(ctx, fmt.Sprintf("%s-%s", api.FEISHU, args.DomainId))
  61. if err != nil {
  62. return errors.Wrap(err, "tenant token invalid && getToken err")
  63. }
  64. header.Set("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[fmt.Sprintf("%s-%s", api.FEISHU, args.DomainId)].Content.AccessToken))
  65. _, err = sendRequest(ctx, ApiSendMessageForFeishuByOpenId, httputils.POST, header, nil, jsonutils.Marshal(body))
  66. if err == nil {
  67. return nil
  68. }
  69. case "99991672": // 未开放发送消息通知的权限
  70. err = errors.Wrap(ErrIncompleteConfig, err.Error())
  71. }
  72. return err
  73. }
  74. // 校验appId与appSecret
  75. func (feishuSender *SFeishuSender) ValidateConfig(ctx context.Context, config api.NotifyConfig) (string, error) {
  76. rep, err := feishuSender.getAccessToken(config.AppId, config.AppSecret)
  77. if err == nil {
  78. return "", nil
  79. }
  80. var msg string
  81. switch rep.Code {
  82. case 10003:
  83. msg = "invalid AppId"
  84. case 10014:
  85. msg = "invalid AppSecret"
  86. }
  87. return msg, err
  88. }
  89. // 根据用户手机号获取用户的open_id
  90. func (feishuSender *SFeishuSender) ContactByMobile(ctx context.Context, mobile, domainId string) (string, error) {
  91. body := jsonutils.NewDict()
  92. body.Set("mobiles", jsonutils.NewArray(jsonutils.NewString(mobile)))
  93. header := http.Header{}
  94. // 考虑到获取用户id需求较少,可通过直接更新token来避免token失效
  95. err := feishuSender.GetAccessToken(ctx, domainId)
  96. if err != nil {
  97. return "", errors.Wrap(err, "GetAccessToken")
  98. }
  99. params := url.Values{}
  100. params.Set("mobiles", mobile)
  101. header.Set("Authorization", fmt.Sprintf("Bearer %s", models.ConfigMap[fmt.Sprintf("%s-%s", api.FEISHU, domainId)].Content.AccessToken))
  102. resp, err := sendRequest(ctx, ApiFetchUserID, httputils.GET, header, params, body)
  103. if err != nil {
  104. return "", err
  105. }
  106. mobileNotExist, _ := resp.GetArray("data", "mobiles_not_exist")
  107. if len(mobileNotExist) != 0 {
  108. return "", errors.Wrapf(errors.ErrNotFound, "no such user whose mobile is %s", mobile)
  109. }
  110. list, err := resp.GetArray("data", "mobile_users", mobile)
  111. if err != nil {
  112. return "", errors.Wrap(err, "jsonutils.JSONObject.GetArray")
  113. }
  114. // len(list) must be positive
  115. userId, err := list[0].GetString("open_id")
  116. if err != nil {
  117. return "", errors.Wrapf(err, "user result:%v", resp)
  118. }
  119. return userId, nil
  120. }
  121. func (feishuSender *SFeishuSender) IsPersonal() bool {
  122. return true
  123. }
  124. func (feishuSender *SFeishuSender) IsRobot() bool {
  125. return false
  126. }
  127. func (feishuSender *SFeishuSender) IsValid() bool {
  128. return len(feishuSender.config) > 0
  129. }
  130. func (feishuSender *SFeishuSender) IsPullType() bool {
  131. return true
  132. }
  133. func (feishuSender *SFeishuSender) IsSystemConfigContactType() bool {
  134. return true
  135. }
  136. func (feishuSender *SFeishuSender) RegisterConfig(config models.SConfig) {
  137. models.ConfigMap[fmt.Sprintf("%s-%s", config.Type, config.DomainId)] = config
  138. }
  139. // 获取token
  140. func (feishuSender *SFeishuSender) GetAccessToken(ctx context.Context, domainId string) error {
  141. key := fmt.Sprintf("%s-%s", api.FEISHU, domainId)
  142. if _, ok := models.ConfigMap[key]; !ok {
  143. return errors.Wrapf(errors.ErrNotSupported, "contact-type:%s,domain_id:%s is missing config", api.FEISHU, domainId)
  144. }
  145. appId, appSecret := models.ConfigMap[key].Content.AppId, models.ConfigMap[key].Content.AppSecret
  146. resp, err := feishuSender.getAccessToken(appId, appSecret)
  147. if err != nil {
  148. return errors.Wrap(err, "get accessToken")
  149. }
  150. models.ConfigMap[key].Content.AccessToken = resp.TenantAccessToken
  151. return nil
  152. }
  153. func (feishuSender *SFeishuSender) getAccessToken(appId, appSecret string) (*feishu.TenantAccesstokenResp, error) {
  154. resp, err := feishu.GetTenantAccessTokenInternal(appId, appSecret)
  155. if err != nil {
  156. return resp, errors.Wrap(err, "feishu.GetTenantAccessTokenInternal")
  157. }
  158. return resp, nil
  159. }
  160. func init() {
  161. models.Register(&SFeishuSender{
  162. config: map[string]api.SNotifyConfigContent{},
  163. })
  164. }