diff --git a/seahub/api2/endpoints/admin/dingtalk.py b/seahub/api2/endpoints/admin/dingtalk.py index 426b95f76f..400af0a541 100644 --- a/seahub/api2/endpoints/admin/dingtalk.py +++ b/seahub/api2/endpoints/admin/dingtalk.py @@ -5,7 +5,6 @@ import logging import requests -from django.core.cache import cache from django.core.files.base import ContentFile from rest_framework.authentication import SessionAuthentication @@ -28,9 +27,8 @@ from seahub.profile.models import Profile from seahub.avatar.models import Avatar from seahub.group.utils import validate_group_name -from seahub.utils import normalize_cache_key -from seahub.dingtalk.settings import ENABLE_DINGTALK, DINGTALK_DEPARTMENT_APP_KEY, \ - DINGTALK_DEPARTMENT_APP_SECRET, DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL, \ +from seahub.dingtalk.utils import dingtalk_get_access_token +from seahub.dingtalk.settings import ENABLE_DINGTALK, \ DINGTALK_DEPARTMENT_LIST_DEPARTMENT_URL, \ DINGTALK_DEPARTMENT_GET_DEPARTMENT_URL, \ DINGTALK_DEPARTMENT_GET_DEPARTMENT_USER_LIST_URL, \ @@ -40,34 +38,6 @@ DEPARTMENT_OWNER = 'system admin' logger = logging.getLogger(__name__) - -def get_dingtalk_access_token(): - - cache_key = normalize_cache_key('DINGTALK_ACCESS_TOKEN') - access_token = cache.get(cache_key, None) - - if not access_token: - - data = { - 'appkey': DINGTALK_DEPARTMENT_APP_KEY, - 'appsecret': DINGTALK_DEPARTMENT_APP_SECRET, - } - resp_json = requests.get(DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL, - params=data).json() - - access_token = resp_json.get('access_token', '') - if not access_token: - logger.error('failed to get dingtalk access_token') - logger.error(data) - logger.error(DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL) - logger.error(resp_json) - return '' - - expires_in = resp_json.get('expires_in', 7200) - cache.set(cache_key, access_token, expires_in) - - return access_token - def update_dingtalk_user_info(email, name, contact_email, avatar_url): # make sure the contact_email is unique @@ -114,7 +84,7 @@ class AdminDingtalkDepartments(APIView): if not request.user.admin_permissions.can_manage_user(): return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') - access_token = get_dingtalk_access_token() + access_token = dingtalk_get_access_token() if not access_token: error_msg = '获取钉钉组织架构失败' return api_error(status.HTTP_404_NOT_FOUND, error_msg) @@ -151,7 +121,7 @@ class AdminDingtalkDepartmentMembers(APIView): if not request.user.admin_permissions.can_manage_user(): return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.') - access_token = get_dingtalk_access_token() + access_token = dingtalk_get_access_token() if not access_token: error_msg = '获取钉钉组织架构成员失败' return api_error(status.HTTP_404_NOT_FOUND, error_msg) @@ -320,7 +290,7 @@ class AdminDingtalkDepartmentsImport(APIView): error_msg = 'department_id invalid.' return api_error(status.HTTP_400_BAD_REQUEST, error_msg) - access_token = get_dingtalk_access_token() + access_token = dingtalk_get_access_token() if not access_token: error_msg = '获取钉钉组织架构失败' return api_error(status.HTTP_404_NOT_FOUND, error_msg) diff --git a/seahub/dingtalk/settings.py b/seahub/dingtalk/settings.py index ca3756702b..3d806defce 100644 --- a/seahub/dingtalk/settings.py +++ b/seahub/dingtalk/settings.py @@ -1,6 +1,8 @@ import seahub.settings as settings ENABLE_DINGTALK = getattr(settings, 'ENABLE_DINGTALK', False) +DINGTALK_AGENT_ID = getattr(settings, 'DINGTALK_AGENT_ID', '') +DINGTALK_GET_USERID_BY_UNIONID = getattr(settings, 'DINGTALK_GET_USERID_BY_UNIONID', 'https://oapi.dingtalk.com/user/getUseridByUnionid') # for dingtalk qr connect DINGTALK_QR_CONNECT_LOGIN_REMEMBER_ME = True @@ -21,3 +23,6 @@ DINGTALK_DEPARTMENT_LIST_DEPARTMENT_URL = getattr(settings, 'DINGTALK_DEPARTMENT DINGTALK_DEPARTMENT_GET_DEPARTMENT_URL = getattr(settings, 'DINGTALK_DEPARTMENT_GET_DEPARTMENT_URL', 'https://oapi.dingtalk.com/department/get') DINGTALK_DEPARTMENT_GET_DEPARTMENT_USER_LIST_URL = getattr(settings, 'DINGTALK_DEPARTMENT_GET_DEPARTMENT_USER_LIST_URL', 'https://oapi.dingtalk.com/user/listbypage') DINGTALK_DEPARTMENT_USER_SIZE = 100 + +# for dingtalk message +DINGTALK_MESSAGE_SEND_TO_CONVERSATION_URL = getattr(settings, 'DINGTALK_MESSAGE_SEND_TO_CONVERSATION_URL', 'https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2') diff --git a/seahub/dingtalk/utils.py b/seahub/dingtalk/utils.py new file mode 100644 index 0000000000..d803f8cb3f --- /dev/null +++ b/seahub/dingtalk/utils.py @@ -0,0 +1,63 @@ +import logging +import requests + +from django.core.cache import cache +from seahub.utils import normalize_cache_key + +from seahub.dingtalk.settings import DINGTALK_DEPARTMENT_APP_KEY, \ + DINGTALK_DEPARTMENT_APP_SECRET, \ + DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL, \ + DINGTALK_GET_USERID_BY_UNIONID + +logger = logging.getLogger(__name__) + +def dingtalk_get_access_token(): + + cache_key = normalize_cache_key('DINGTALK_ACCESS_TOKEN') + access_token = cache.get(cache_key, None) + if access_token: + return access_token + + data = { + 'appkey': DINGTALK_DEPARTMENT_APP_KEY, + 'appsecret': DINGTALK_DEPARTMENT_APP_SECRET, + } + resp_json = requests.get(DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL, + params=data).json() + + access_token = resp_json.get('access_token', '') + if not access_token: + logger.error('failed to get dingtalk access_token') + logger.error(DINGTALK_DEPARTMENT_GET_ACCESS_TOKEN_URL) + logger.error(data) + logger.error(resp_json) + return '' + + expires_in = resp_json.get('expires_in', 7200) + cache.set(cache_key, access_token, expires_in) + + return access_token + +def dingtalk_get_userid_by_unionid(union_id): + + cache_key = normalize_cache_key('DINGTALK_UNION_ID_%s' % union_id) + user_id = cache.get(cache_key, None) + if user_id: + return user_id + + access_token = dingtalk_get_access_token() + data = { + 'access_token': access_token, + 'unionid': union_id, + } + resp_json = requests.get(DINGTALK_GET_USERID_BY_UNIONID, params=data).json() + user_id = resp_json.get('userid', '') + if not user_id: + logger.error('failed to get userid by unionid: %s' % union_id) + logger.error(DINGTALK_GET_USERID_BY_UNIONID) + logger.error(data) + logger.error(resp_json) + return '' + + cache.set(cache_key, user_id) + return user_id diff --git a/seahub/notifications/management/commands/send_notices_to_social_account.py b/seahub/notifications/management/commands/send_notices_to_social_account.py new file mode 100644 index 0000000000..60d84a4000 --- /dev/null +++ b/seahub/notifications/management/commands/send_notices_to_social_account.py @@ -0,0 +1,240 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 +from datetime import datetime +import logging +import re +import json +import requests + +from django.core.management.base import BaseCommand +from django.core.urlresolvers import reverse +from django.utils import translation +from django.utils.translation import ugettext as _ + +from seahub.base.models import CommandsLastCheck +from seahub.notifications.models import UserNotification +from seahub.utils import get_site_scheme_and_netloc, get_site_name +from seahub.auth.models import SocialAuthUser + +from seahub.dingtalk.utils import dingtalk_get_access_token, dingtalk_get_userid_by_unionid +from seahub.dingtalk.settings import DINGTALK_MESSAGE_SEND_TO_CONVERSATION_URL, \ + DINGTALK_AGENT_ID, ENABLE_DINGTALK + +from seahub.work_weixin.utils import get_work_weixin_access_token, handler_work_weixin_api_response +from seahub.work_weixin.settings import WORK_WEIXIN_NOTIFICATIONS_URL, \ + WORK_WEIXIN_PROVIDER, WORK_WEIXIN_UID_PREFIX, WORK_WEIXIN_AGENT_ID, ENABLE_WORK_WEIXIN + +# Get an instance of a logger +logger = logging.getLogger(__name__) + +########## Utility Functions ########## + +# https://ding-doc.dingtalk.com/doc#/serverapi3/wvdxel +def remove_html_a_element_for_dingtalk(s): + """ + Replace xx to xx and wrap content with
. + """ + patt = '(.+?)' + + def repl(matchobj): + return matchobj.group(1) + + return re.sub(patt, repl, s) + +# https://work.weixin.qq.com/api/doc#90000/90135/90236/ +def wrap_div_for_work_weixin(s): + """ + Replace xx to xx and wrap content with
. + """ + patt = '(.+?)' + + def repl(matchobj): + return matchobj.group(1) + + return '
' + re.sub(patt, repl, s) + '
' + +class CommandLogMixin(object): + + def println(self, msg): + self.stdout.write('[%s] %s\n' % (str(datetime.now()), msg)) + + def log_error(self, msg): + logger.error(msg) + self.println(msg) + + def log_info(self, msg): + logger.info(msg) + self.println(msg) + + def log_debug(self, msg): + logger.debug(msg) + self.println(msg) + + +class Command(BaseCommand, CommandLogMixin): + """ send dingtalk/work weixin notifications + """ + + help = "Send notices to user's social account if he/she has unseen notices every period of time." + label = "notifications_send_notices_to_social_account" + + def handle(self, *args, **options): + + if ENABLE_DINGTALK: + self.log_debug('Start sending dingtalk msg...') + if ENABLE_WORK_WEIXIN: + self.log_debug('Start sending work weixin msg...') + + self.do_action() + + if ENABLE_DINGTALK: + self.log_debug('Finish sending dingtalk msg.\n') + if ENABLE_WORK_WEIXIN: + self.log_debug('Finish sending work weixin msg.\n') + + def send_dingtalk_msg(self, user_id, title, content): + + self.log_info('Send dingtalk msg to user: %s, msg: %s' % (user_id, content)) + data = { + "agent_id": DINGTALK_AGENT_ID, + "userid_list": user_id, + "msg": { + "msgtype": "markdown", + "markdown": { + "title": title, + "text": content + } + } + } + resp_json = requests.post(self.dingtalk_message_send_to_conversation_url, + data=json.dumps(data)).json() + if resp_json.get('errcode') != 0: + self.log_info(resp_json) + + def send_work_weixin_msg(self, uid, title, content): + + self.log_info('Send wechat msg to user: %s, msg: %s' % (uid, content)) + + data = { + "touser": uid, + "agentid": WORK_WEIXIN_AGENT_ID, + 'msgtype': 'textcard', + 'textcard': { + 'title': title, + 'description': content, + 'url': self.detail_url, + }, + } + + api_response = requests.post(self.work_weixin_notifications_url, json=data) + api_response_dic = handler_work_weixin_api_response(api_response) + if api_response_dic: + self.log_info(api_response_dic) + else: + self.log_error('can not get work weixin notifications API response') + + def do_action(self): + + if not ENABLE_DINGTALK and not ENABLE_WORK_WEIXIN: + self.log_info('No social account enabled') + return + + dingtalk_access_token = '' + work_weixin_access_token = '' + + if ENABLE_DINGTALK: + + dingtalk_access_token = dingtalk_get_access_token() + if not dingtalk_access_token: + self.log_error('can not get access token for dingtalk') + else: + self.dingtalk_message_send_to_conversation_url = DINGTALK_MESSAGE_SEND_TO_CONVERSATION_URL + \ + '?access_token=' + dingtalk_access_token + self.detail_url = get_site_scheme_and_netloc().rstrip('/') + reverse('user_notification_list') + + if ENABLE_WORK_WEIXIN: + + work_weixin_access_token = get_work_weixin_access_token() + if not work_weixin_access_token: + self.log_error('can not get access token for work weixin') + else: + self.work_weixin_notifications_url = WORK_WEIXIN_NOTIFICATIONS_URL + \ + '?access_token=' + work_weixin_access_token + self.detail_url = get_site_scheme_and_netloc().rstrip('/') + reverse('user_notification_list') + + if not dingtalk_access_token and not work_weixin_access_token: + return + + # save current language + cur_language = translation.get_language() + # active zh-cn + translation.activate('zh-cn') + self.log_info('the language is set to zh-cn') + + # 1. get previous time that command last runs + now = datetime.now() + today = datetime.now().replace(hour=0).replace(minute=0).replace(second=0).replace(microsecond=0) + + try: + cmd_last_check = CommandsLastCheck.objects.get(command_type=self.label) + self.log_debug('Last check time is %s' % cmd_last_check.last_check) + + last_check_dt = cmd_last_check.last_check + + cmd_last_check.last_check = now + cmd_last_check.save() + except CommandsLastCheck.DoesNotExist: + last_check_dt = today + self.log_debug('Create new last check time: %s' % now) + CommandsLastCheck(command_type=self.label, last_check=now).save() + + # 2. get all unseen notices + user_notifications = UserNotification.objects.filter(timestamp__gt=last_check_dt).filter(seen=False) + self.log_info('Found %d notices' % user_notifications.count()) + if user_notifications.count() == 0: + return + + # 3. get all users should send notice to + user_email_list = list(set([item.to_user for item in user_notifications])) + + dingtail_socials = SocialAuthUser.objects.filter(provider='dingtalk').filter(username__in=user_email_list) + dingtalk_email_list = [item.username for item in dingtail_socials] + dingtalk_email_uid_dict = {} + for item in dingtail_socials: + dingtalk_email_uid_dict[item.username] = dingtalk_get_userid_by_unionid(item.uid) + + work_weixin_socials = SocialAuthUser.objects.filter(provider=WORK_WEIXIN_PROVIDER, \ + uid__contains=WORK_WEIXIN_UID_PREFIX).filter(username__in=user_email_list) + work_weixin_email_list = [item.username for item in work_weixin_socials] + work_weixin_email_uid_dict = {} + for item in work_weixin_socials: + work_weixin_email_uid_dict[item.username] = item.uid[len(WORK_WEIXIN_UID_PREFIX):] + + # 4. send msg + site_name = get_site_name() + + for email in list(set(dingtalk_email_list + work_weixin_email_list)): + + should_send = [] + for notification in user_notifications: + if email == notification.to_user: + should_send.append(notification) + + title = _("You've got %(num)s new notices on %(site_name)s:\n") % \ + {'num': len(should_send), 'site_name': site_name, } + + has_sent = False + + if not has_sent and email in dingtalk_email_list and ENABLE_DINGTALK: + content = ' \n '.join([remove_html_a_element_for_dingtalk(x.format_msg()) for x in should_send]) + self.send_dingtalk_msg(dingtalk_email_uid_dict[email], title, content) + has_sent = True + + if not has_sent and email in work_weixin_email_list and ENABLE_WORK_WEIXIN: + content = ''.join([wrap_div_for_work_weixin(x.format_msg()) for x in should_send]) + self.send_work_weixin_msg(work_weixin_email_uid_dict[email], title, content) + has_sent = True + + translation.activate(cur_language) + self.log_info('reset language success') + return diff --git a/seahub/notifications/management/commands/send_work_weixin_notifications.py b/seahub/notifications/management/commands/send_work_weixin_notifications.py deleted file mode 100644 index aca6bebb38..0000000000 --- a/seahub/notifications/management/commands/send_work_weixin_notifications.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright (c) 2012-2019 Seafile Ltd. -# encoding: utf-8 -from datetime import datetime -import logging -import re -import requests - -from django.core.management.base import BaseCommand -from django.core.urlresolvers import reverse -from django.utils import translation -from django.utils.translation import ungettext - -from seahub.base.models import CommandsLastCheck -from seahub.notifications.models import UserNotification -from seahub.utils import get_site_scheme_and_netloc, get_site_name -from seahub.auth.models import SocialAuthUser -from seahub.work_weixin.utils import work_weixin_notifications_check, \ - get_work_weixin_access_token, handler_work_weixin_api_response -from seahub.work_weixin.settings import WORK_WEIXIN_NOTIFICATIONS_URL, \ - WORK_WEIXIN_PROVIDER, WORK_WEIXIN_UID_PREFIX, WORK_WEIXIN_AGENT_ID - -# Get an instance of a logger -logger = logging.getLogger(__name__) - - -# https://work.weixin.qq.com/api/doc#90000/90135/90236/ - -########## Utility Functions ########## -def wrap_div(s): - """ - Replace xx to xx and wrap content with
. - """ - patt = '(.+?)' - - def repl(matchobj): - return matchobj.group(1) - - return '
' + re.sub(patt, repl, s) + '
' - - -class CommandLogMixin(object): - def println(self, msg): - self.stdout.write('[%s] %s\n' % (str(datetime.now()), msg)) - - def log_error(self, msg): - logger.error(msg) - self.println(msg) - - def log_info(self, msg): - logger.info(msg) - self.println(msg) - - def log_debug(self, msg): - logger.debug(msg) - self.println(msg) - - -####################################### - -class Command(BaseCommand, CommandLogMixin): - """ send work weixin notifications - """ - - help = 'Send WeChat Work msg to user if he/she has unseen notices every ' - 'period of time.' - label = "notifications_send_wxwork_notices" - - def handle(self, *args, **options): - self.log_debug('Start sending work weixin msg...') - self.do_action() - self.log_debug('Finish sending work weixin msg.\n') - - def send_work_weixin_msg(self, uid, title, content): - - self.log_info('Send wechat msg to user: %s, msg: %s' % (uid, content)) - - data = { - "touser": uid, - "agentid": WORK_WEIXIN_AGENT_ID, - 'msgtype': 'textcard', - 'textcard': { - 'title': title, - 'description': content, - 'url': self.detail_url, - }, - } - - api_response = requests.post(self.work_weixin_notifications_url, json=data) - api_response_dic = handler_work_weixin_api_response(api_response) - if api_response_dic: - self.log_info(api_response_dic) - else: - self.log_error('can not get work weixin notifications API response') - - def do_action(self): - # check before start - if not work_weixin_notifications_check(): - self.log_error('work weixin notifications settings check failed') - return - - access_token = get_work_weixin_access_token() - if not access_token: - self.log_error('can not get access_token') - - self.work_weixin_notifications_url = WORK_WEIXIN_NOTIFICATIONS_URL + '?access_token=' + access_token - self.detail_url = get_site_scheme_and_netloc().rstrip('/') + reverse('user_notification_list') - site_name = get_site_name() - - # start - now = datetime.now() - today = datetime.now().replace(hour=0).replace(minute=0).replace( - second=0).replace(microsecond=0) - - # 1. get all users who are connected work weixin - socials = SocialAuthUser.objects.filter(provider=WORK_WEIXIN_PROVIDER, uid__contains=WORK_WEIXIN_UID_PREFIX) - users = [(x.username, x.uid[len(WORK_WEIXIN_UID_PREFIX):]) for x in socials] - self.log_info('Found %d users' % len(users)) - if not users: - return - - user_uid_map = {} - for username, uid in users: - user_uid_map[username] = uid - - # 2. get previous time that command last runs - try: - cmd_last_check = CommandsLastCheck.objects.get(command_type=self.label) - self.log_debug('Last check time is %s' % cmd_last_check.last_check) - - last_check_dt = cmd_last_check.last_check - - cmd_last_check.last_check = now - cmd_last_check.save() - except CommandsLastCheck.DoesNotExist: - last_check_dt = today - self.log_debug('Create new last check time: %s' % now) - CommandsLastCheck(command_type=self.label, last_check=now).save() - - # 3. get all unseen notices for those users - qs = UserNotification.objects.filter( - timestamp__gt=last_check_dt - ).filter(seen=False).filter( - to_user__in=list(user_uid_map.keys()) - ) - self.log_info('Found %d notices' % qs.count()) - if qs.count() == 0: - return - - user_notices = {} - for q in qs: - if q.to_user not in user_notices: - user_notices[q.to_user] = [q] - else: - user_notices[q.to_user].append(q) - - # save current language - cur_language = translation.get_language() - # active zh-cn - translation.activate('zh-cn') - self.log_info('the language is set to zh-cn') - - # 4. send msg to users - for username, uid in users: - notices = user_notices.get(username, []) - count = len(notices) - if count == 0: - continue - - title = ungettext( - "\n" - "You've got 1 new notice on %(site_name)s:\n", - "\n" - "You've got %(num)s new notices on %(site_name)s:\n", - count - ) % {'num': count, 'site_name': site_name, } - - content = ''.join([wrap_div(x.format_msg()) for x in notices]) - self.send_work_weixin_msg(uid, title, content) - - # reset language - translation.activate(cur_language) - self.log_info('reset language success')