2021-03-24 11:01:35 +00:00
|
|
|
|
from typing import Iterable, AnyStr
|
|
|
|
|
|
|
|
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
|
from rest_framework.exceptions import APIException
|
|
|
|
|
|
2023-04-28 06:01:44 +00:00
|
|
|
|
from users.utils import construct_user_email
|
2021-03-24 11:01:35 +00:00
|
|
|
|
from common.utils.common import get_logger
|
2023-04-28 06:01:44 +00:00
|
|
|
|
from common.sdk.im.utils import digest, update_values
|
2021-10-21 08:50:11 +00:00
|
|
|
|
from common.sdk.im.mixin import RequestMixin, BaseRequest
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
|
|
|
|
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'
|
2023-05-27 10:01:37 +00:00
|
|
|
|
QR_CONNECT = 'https://login.work.weixin.qq.com/wwlogin/sso/login'
|
2022-03-29 05:19:13 +00:00
|
|
|
|
OAUTH_CONNECT = 'https://open.weixin.qq.com/connect/oauth2/authorize'
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
|
|
|
|
# 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全部非法或无权限。
|
|
|
|
|
|
2021-05-17 06:20:51 +00:00
|
|
|
|
# https: // open.work.weixin.qq.com / devtool / query?e = 82001
|
|
|
|
|
RECIPIENTS_EMPTY = 82001 # 指定的成员/部门/标签全部为空
|
|
|
|
|
|
2021-03-24 11:01:35 +00:00
|
|
|
|
# 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 无效时重试
|
|
|
|
|
"""
|
2021-08-12 08:44:06 +00:00
|
|
|
|
invalid_token_errcodes = (ErrorCode.INVALID_TOKEN,)
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
|
|
|
|
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
2021-10-18 03:25:39 +00:00
|
|
|
|
self._corpid = corpid or ''
|
|
|
|
|
self._corpsecret = corpsecret or ''
|
|
|
|
|
self._agentid = agentid or ''
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2021-08-12 08:44:06 +00:00
|
|
|
|
def add_token(self, kwargs: dict):
|
|
|
|
|
params = kwargs.get('params')
|
|
|
|
|
if params is None:
|
2021-03-24 11:01:35 +00:00
|
|
|
|
params = {}
|
2021-08-12 08:44:06 +00:00
|
|
|
|
kwargs['params'] = params
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
2021-08-12 08:44:06 +00:00
|
|
|
|
params['access_token'] = self.access_token
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WeCom(RequestMixin):
|
|
|
|
|
"""
|
|
|
|
|
非业务数据导致的错误直接抛异常,说明是系统配置错误,业务代码不用理会
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, corpid, corpsecret, agentid, timeout=None):
|
2021-10-18 03:25:39 +00:00
|
|
|
|
self._corpid = corpid or ''
|
|
|
|
|
self._corpsecret = corpsecret or ''
|
|
|
|
|
self._agentid = agentid or ''
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
|
|
|
|
self._requests = WeComRequests(
|
|
|
|
|
corpid=corpid,
|
|
|
|
|
corpsecret=corpsecret,
|
|
|
|
|
agentid=agentid,
|
|
|
|
|
timeout=timeout
|
|
|
|
|
)
|
|
|
|
|
|
2021-12-15 07:01:46 +00:00
|
|
|
|
def send_markdown(self, users: Iterable, msg: AnyStr, **kwargs):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
def send_text(self, users: Iterable, msg: AnyStr, markdown=False, **kwargs):
|
2021-03-24 11:01:35 +00:00
|
|
|
|
"""
|
|
|
|
|
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
|
|
|
|
|
}
|
2021-12-15 07:01:46 +00:00
|
|
|
|
if markdown:
|
|
|
|
|
body['msgtype'] = 'markdown'
|
|
|
|
|
body["markdown"] = {
|
|
|
|
|
"content": msg
|
|
|
|
|
}
|
|
|
|
|
body.pop('text', '')
|
|
|
|
|
|
2021-08-24 06:20:54 +00:00
|
|
|
|
logger.info(f'Wecom send text: users={users} msg={msg}')
|
2021-03-24 11:01:35 +00:00
|
|
|
|
data = self._requests.post(URL.SEND_MESSAGE, json=body, check_errcode_is_0=False)
|
|
|
|
|
|
|
|
|
|
errcode = data['errcode']
|
2021-05-17 06:20:51 +00:00
|
|
|
|
if errcode in (ErrorCode.RECIPIENTS_INVALID, ErrorCode.RECIPIENTS_EMPTY):
|
2021-03-24 11:01:35 +00:00
|
|
|
|
# 全部接收人无权限或不存在
|
|
|
|
|
return users
|
2021-08-12 08:44:06 +00:00
|
|
|
|
self._requests.check_errcode_is_0(data)
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
2021-12-13 07:52:54 +00:00
|
|
|
|
if 'invaliduser' not in data:
|
|
|
|
|
return ()
|
|
|
|
|
|
2021-03-24 11:01:35 +00:00
|
|
|
|
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
|
2023-04-28 06:01:44 +00:00
|
|
|
|
params = {'code': code}
|
2021-03-24 11:01:35 +00:00
|
|
|
|
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
|
|
|
|
|
|
2021-08-12 08:44:06 +00:00
|
|
|
|
self._requests.check_errcode_is_0(data)
|
2021-03-24 11:01:35 +00:00
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
2023-04-28 06:01:44 +00:00
|
|
|
|
def get_user_detail(self, user_id, **kwargs):
|
2021-03-24 11:01:35 +00:00
|
|
|
|
# https://open.work.weixin.qq.com/api/doc/90000/90135/90196
|
2023-04-28 06:01:44 +00:00
|
|
|
|
params = {'userid': user_id}
|
|
|
|
|
data = self._requests.get(URL.GET_USER_DETAIL, params)
|
|
|
|
|
username = data.get('userid')
|
|
|
|
|
name = data.get('name', username)
|
|
|
|
|
email = data.get('email') or data.get('biz_mail')
|
|
|
|
|
email = construct_user_email(username, email)
|
|
|
|
|
return {
|
|
|
|
|
'username': username, 'name': name, 'email': email
|
2021-03-24 11:01:35 +00:00
|
|
|
|
}
|
|
|
|
|
|