jumpserver/apps/common/message/backends/wecom/__init__.py

198 lines
6.2 KiB
Python

from typing import Iterable, AnyStr
from django.utils.translation import ugettext_lazy as _
from rest_framework.exceptions import APIException
from requests.exceptions import ReadTimeout
import requests
from django.core.cache import cache
from common.utils.common import get_logger
from common.message.backends.utils import digest, DictWrapper, update_values, set_default
from common.message.backends.utils import request
from common.message.backends.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'
# 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_errcode = ErrorCode.INVALID_TOKEN
def __init__(self, corpid, corpsecret, agentid, timeout=None):
self._corpid = corpid
self._corpsecret = corpsecret
self._agentid = agentid
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
@request
def get(self, url, params=None, with_token=True,
check_errcode_is_0=True, **kwargs):
# self.request ...
pass
@request
def post(self, url, params=None, json=None,
with_token=True, check_errcode_is_0=True,
**kwargs):
# self.request ...
pass
def request(self, method, url,
params=None,
with_token=True,
check_errcode_is_0=True,
**kwargs):
if not isinstance(params, dict):
params = {}
if with_token:
params['access_token'] = self.access_token
data = self.raw_request(method, url, params=params, **kwargs)
if check_errcode_is_0:
self.check_errcode_is_0(data)
return data
class WeCom(RequestMixin):
"""
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
"""
def __init__(self, corpid, corpsecret, agentid, timeout=None):
self._corpid = corpid
self._corpsecret = corpsecret
self._agentid = agentid
self._requests = WeComRequests(
corpid=corpid,
corpsecret=corpsecret,
agentid=agentid,
timeout=timeout
)
def send_text(self, users: Iterable, msg: AnyStr, **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
}
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.check_errcode_is_0(data)
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.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