mirror of https://github.com/jumpserver/jumpserver
				
				
				
			
		
			
				
	
	
		
			187 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Python
		
	
	
			
		
		
	
	
			187 lines
		
	
	
		
			5.9 KiB
		
	
	
	
		
			Python
		
	
	
| from typing import Iterable, AnyStr
 | |
| 
 | |
| from django.utils.translation import ugettext_lazy as _
 | |
| from rest_framework.exceptions import APIException
 | |
| 
 | |
| from common.utils.common import get_logger
 | |
| from common.sdk.im.utils import digest, DictWrapper, update_values, set_default
 | |
| from common.sdk.im.mixin import RequestMixin, BaseRequest
 | |
| 
 | |
| logger = get_logger(__name__)
 | |
| 
 | |
| 
 | |
| class WeComError(APIException):
 | |
|     default_code = 'wecom_error'
 | |
|     default_detail = _('WeCom error, please contact system administrator')
 | |
| 
 | |
| 
 | |
| class URL:
 | |
|     GET_TOKEN = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken'
 | |
|     SEND_MESSAGE = 'https://qyapi.weixin.qq.com/cgi-bin/message/send'
 | |
|     QR_CONNECT = 'https://open.work.weixin.qq.com/wwopen/sso/qrConnect'
 | |
|     OAUTH_CONNECT = 'https://open.weixin.qq.com/connect/oauth2/authorize'
 | |
| 
 | |
|     # https://open.work.weixin.qq.com/api/doc/90000/90135/91437
 | |
|     GET_USER_ID_BY_CODE = 'https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo'
 | |
|     GET_USER_DETAIL = 'https://qyapi.weixin.qq.com/cgi-bin/user/get'
 | |
| 
 | |
| 
 | |
| class ErrorCode:
 | |
|     # https://open.work.weixin.qq.com/api/doc/90000/90139/90313#%E9%94%99%E8%AF%AF%E7%A0%81%EF%BC%9A81013
 | |
|     RECIPIENTS_INVALID = 81013  # UserID、部门ID、标签ID全部非法或无权限。
 | |
| 
 | |
|     # https: // open.work.weixin.qq.com / devtool / query?e = 82001
 | |
|     RECIPIENTS_EMPTY = 82001  # 指定的成员/部门/标签全部为空
 | |
| 
 | |
|     # https://open.work.weixin.qq.com/api/doc/90000/90135/91437
 | |
|     INVALID_CODE = 40029
 | |
| 
 | |
|     INVALID_TOKEN = 40014  # 无效的 access_token
 | |
| 
 | |
| 
 | |
| class WeComRequests(BaseRequest):
 | |
|     """
 | |
|     处理系统级错误,抛出 API 异常,直接生成 HTTP 响应,业务代码无需关心这些错误
 | |
|     - 确保 status_code == 200
 | |
|     - 确保 access_token 无效时重试
 | |
|     """
 | |
|     invalid_token_errcodes = (ErrorCode.INVALID_TOKEN,)
 | |
| 
 | |
|     def __init__(self, corpid, corpsecret, agentid, timeout=None):
 | |
|         self._corpid = corpid or ''
 | |
|         self._corpsecret = corpsecret or ''
 | |
|         self._agentid = agentid or ''
 | |
| 
 | |
|         super().__init__(timeout=timeout)
 | |
| 
 | |
|     def get_access_token_cache_key(self):
 | |
|         return digest(self._corpid, self._corpsecret)
 | |
| 
 | |
|     def request_access_token(self):
 | |
|         params = {'corpid': self._corpid, 'corpsecret': self._corpsecret}
 | |
|         data = self.raw_request('get', url=URL.GET_TOKEN, params=params)
 | |
| 
 | |
|         access_token = data['access_token']
 | |
|         expires_in = data['expires_in']
 | |
|         return access_token, expires_in
 | |
| 
 | |
|     def add_token(self, kwargs: dict):
 | |
|         params = kwargs.get('params')
 | |
|         if params is None:
 | |
|             params = {}
 | |
|             kwargs['params'] = params
 | |
| 
 | |
|         params['access_token'] = self.access_token
 | |
| 
 | |
| 
 | |
| class WeCom(RequestMixin):
 | |
|     """
 | |
|     非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
 | |
|     """
 | |
| 
 | |
|     def __init__(self, corpid, corpsecret, agentid, timeout=None):
 | |
|         self._corpid = corpid or ''
 | |
|         self._corpsecret = corpsecret or ''
 | |
|         self._agentid = agentid or ''
 | |
| 
 | |
|         self._requests = WeComRequests(
 | |
|             corpid=corpid,
 | |
|             corpsecret=corpsecret,
 | |
|             agentid=agentid,
 | |
|             timeout=timeout
 | |
|         )
 | |
| 
 | |
|     def send_markdown(self, users: Iterable, msg: AnyStr, **kwargs):
 | |
|         pass
 | |
| 
 | |
|     def send_text(self, users: Iterable, msg: AnyStr, markdown=False, **kwargs):
 | |
|         """
 | |
|         https://open.work.weixin.qq.com/api/doc/90000/90135/90236
 | |
| 
 | |
|         对于业务代码,只需要关心由 用户id 或 消息不对 导致的错误,其他错误不予理会
 | |
|         """
 | |
|         users = tuple(users)
 | |
| 
 | |
|         extra_params = {
 | |
|             "safe": 0,
 | |
|             "enable_id_trans": 0,
 | |
|             "enable_duplicate_check": 0,
 | |
|             "duplicate_check_interval": 1800
 | |
|         }
 | |
|         update_values(extra_params, kwargs)
 | |
| 
 | |
|         body = {
 | |
|            "touser": '|'.join(users),
 | |
|            "msgtype": "text",
 | |
|            "agentid": self._agentid,
 | |
|            "text": {
 | |
|                "content": msg
 | |
|            },
 | |
|            **extra_params
 | |
|         }
 | |
|         if markdown:
 | |
|             body['msgtype'] = 'markdown'
 | |
|             body["markdown"] = {
 | |
|                 "content": msg
 | |
|             }
 | |
|             body.pop('text', '')
 | |
| 
 | |
|         logger.info(f'Wecom send text: users={users} msg={msg}')
 | |
|         data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False)
 | |
| 
 | |
|         errcode = data['errcode']
 | |
|         if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY):
 | |
|             # 全部接收人无权限或不存在
 | |
|             return users
 | |
|         self._requests.check_errcode_is_0(data)
 | |
| 
 | |
|         if 'invaliduser' not in data:
 | |
|             return ()
 | |
| 
 | |
|         invaliduser = data['invaliduser']
 | |
|         if not invaliduser:
 | |
|             return ()
 | |
| 
 | |
|         if isinstance(invaliduser, str):
 | |
|             logger.error(f'WeCom send text 200, but invaliduser is not str: invaliduser={invaliduser}')
 | |
|             raise WeComError
 | |
| 
 | |
|         invalid_users = invaliduser.split('|')
 | |
|         return invalid_users
 | |
| 
 | |
|     def get_user_id_by_code(self, code):
 | |
|         # # https://open.work.weixin.qq.com/api/doc/90000/90135/91437
 | |
| 
 | |
|         params = {
 | |
|             'code': code,
 | |
|         }
 | |
|         data = self._requests.get(URL.GET_USER_ID_BY_CODE, params=params, check_errcode_is_0=False)
 | |
| 
 | |
|         errcode = data['errcode']
 | |
|         if errcode == ErrorCode.INVALID_CODE:
 | |
|             logger.warn(f'WeCom get_user_id_by_code invalid code: code={code}')
 | |
|             return None, None
 | |
| 
 | |
|         self._requests.check_errcode_is_0(data)
 | |
| 
 | |
|         USER_ID = 'UserId'
 | |
|         OPEN_ID = 'OpenId'
 | |
| 
 | |
|         if USER_ID in data:
 | |
|             return data[USER_ID], USER_ID
 | |
|         elif OPEN_ID in data:
 | |
|             return data[OPEN_ID], OPEN_ID
 | |
|         else:
 | |
|             logger.error(f'WeCom response 200 but get field from json error: fields=UserId|OpenId')
 | |
|             raise WeComError
 | |
| 
 | |
|     def get_user_detail(self, id):
 | |
|         # https://open.work.weixin.qq.com/api/doc/90000/90135/90196
 | |
| 
 | |
|         params = {
 | |
|             'userid': id,
 | |
|         }
 | |
| 
 | |
|         data = self._requests.get(URL.GET_USER_DETAIL, params)
 | |
|         return data
 |