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_dingtalk_notifications.py b/seahub/notifications/management/commands/send_dingtalk_notifications.py new file mode 100644 index 0000000000..ae8157c717 --- /dev/null +++ b/seahub/notifications/management/commands/send_dingtalk_notifications.py @@ -0,0 +1,179 @@ +# Copyright (c) 2012-2019 Seafile Ltd. +# encoding: utf-8 +from datetime import datetime +import logging +import re +import requests +import json + +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.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 + +# Get an instance of a logger +logger = logging.getLogger(__name__) + + +# https://ding-doc.dingtalk.com/doc#/serverapi3/wvdxel + +########## Utility Functions ########## +def remove_html_a_element(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 notifications + """ + + help = 'Send dingtalk msg to user if he/she has unseen notices every ' + 'period of time.' + label = "notifications_send_dingtalk_notices" + + def handle(self, *args, **options): + self.log_debug('Start sending dingtalk msg...') + self.do_action() + self.log_debug('Finish sending dingtalk 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 do_action(self): + + # check before start + access_token = dingtalk_get_access_token() + if not access_token: + self.log_error('can not get access_token') + + self.dingtalk_message_send_to_conversation_url = DINGTALK_MESSAGE_SEND_TO_CONVERSATION_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 dingtalk + socials = SocialAuthUser.objects.filter(provider='dingtalk') + users = [(x.username, x.uid) 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] = dingtalk_get_userid_by_unionid(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: + user_id = user_uid_map[username] + 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 = ' \n '.join([remove_html_a_element(x.format_msg()) for x in notices]) + self.send_dingtalk_msg(user_id, title, content) + + # reset language + translation.activate(cur_language) + self.log_info('reset language success')