├── .DS_Store ├── .gitignore ├── .idea ├── .DS_Store ├── inspectionProfiles │ └── profiles_settings.xml ├── misc.xml ├── modules.xml ├── odoo_dingding.iml └── workspace.xml ├── README.md └── dingding ├── .DS_Store ├── __init__.py ├── __manifest__.py ├── controller ├── __init__.py ├── __init__.pyc ├── page_controller.py └── page_controller.pyc ├── ding_api.py ├── ding_api.pyc ├── ding_model.py ├── dingtalk_crypto ├── __init__.py ├── crypto.py ├── pkcs7.py └── utils.py ├── security └── ir.model.access.csv ├── static └── src │ ├── css │ └── weui3.css │ └── js │ └── dingding.js ├── views ├── ding_model.xml └── res_users.xml └── 钉钉使用手册.docx /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbert-yuan/odoo_dingding/9711a27b5e7308093a90507617e90e0b6a5d0c8f/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .vscode 3 | *.swp 4 | *.idea 5 | .DS_store 6 | -------------------------------------------------------------------------------- /.idea/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbert-yuan/odoo_dingding/9711a27b5e7308093a90507617e90e0b6a5d0c8f/.idea/.DS_Store -------------------------------------------------------------------------------- /.idea/inspectionProfiles/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | AngularJS 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/odoo_dingding.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/workspace.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | 18 | 1515646660063 19 | 23 | 24 | 25 | 26 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![996.icu](https://img.shields.io/badge/link-996.icu-red.svg) 2 | # odoo_dingding(最新支持11.0) 3 | 即将实现功能 4 | ``` 5 | ··重点 记得配置 钉钉应用里面的白名单 ‘服务器公网出口IP名单’ 否则不能正常的访问 6 | ``` 7 | ## 简单配置(暂时只支持单个应用)上面钉钉里面配置对应odoo里面配置 8 | ```python 9 | 钉钉开发这用户 /E应用/应用开发 中单个应用 AppKey AppSecret AgentId 10 | odoo中 /配置/用户/钉钉 表单 钉钉AppKey 钉钉AppSecret agent_id 11 | ``` 12 | 点击获取 => 获取部门 => 点击获取用户 如果钉钉里面有部门和用户的话你就可以在 odoo中菜单 /配置/用户/钉钉部门 /配置/用户/钉钉用户中看到对应的信息 13 | 在钉钉用户 记录信息的最后有一个按钮,如果你 绑定的有系统用户的话点击发消息给他,相应的用户就会收到一条随机消息。-参考发消息的按钮的方法就可以学会如何用钉钉发消息了。 14 | 15 | 16 | 17 | 18 | ## 最新发现钉钉更新了API 按照目前的设计不符合新的API 请注意。(10.0 分支已更新部分功能,master待更新) 19 | ```xml 20 | 从2018.12.17开始,企业内部开发进行账号升级,不再支持创建corpSecret(已有的corpSecret可以继续使用)。企业内部开发后续可以通过创建应用来自动生成appKey和appSecret,然后开发者可以获取access_token,具体详情参考以下文档。 21 | ``` 22 | 23 | ## TO-DO-LIST 24 | - [X] 扫码登陆( 已完成 |master ) 25 | - [ ] 员工同步及公司部门构架同步的完善(初步功能已经完成|master|10.0)。(待完善) 26 | - [ ] 钉钉上新建员工同步到系统中(通过回调实现,在企业微信中,启用员工api管理后就关闭了企业微信后台修改新建的权限,所以这点是钉钉的不方便的地方,双向同步比较麻烦)。(待完善) 27 | - [ ] 部门和同步,新建删除【具体设计待定,具体和系统部门绑定关系怎么处理待定】。(待完善) 28 | 29 | - [X] 支持多个agent。(已完成|master) 30 | - [X] 单独开启线程获取token 不是用的时候才去获取。(已完成|master) 31 | - [X] 可以用多个agent 进行发送消息。(已完成|master) 32 | - [X] 注册钉钉回调事件。(已完成|master|10.0) 33 | - [X] 代码发起钉钉相关的审批。(已完成|master|10.0) 34 | - [X] 代码监控(通过回调)审批单据的状态。(已完成|master|10.0) 35 | - [X] 钉钉中jsAPi 相关的代码(包含, jsapi 中要用的签名等信息,本代码中没有提供完整示例|master|10.0)。(已完成) 36 | - [ ] 对接钉钉E应用基本代码及demo。(待完善|master|10.0) 37 | - [ ] 尽可能对接更多的钉钉提供api的功能具体对接什么看心情吧。(待完善) 38 | 39 | --------------------------------------------------- 40 | odoo10 对接钉钉部分功能 41 | 功能仅供参考,如要进行实际应用请,核对后再进行操作。 42 | 效果图参见ISSUE(审批模版是要钉钉配置创建的)https://github.com/gilbert-yuan/odoo_dingding/issues/5 43 | 44 | 1.详细说明及使用介绍参见 https://github.com/gilbert-yuan/odoo_dingding/blob/master/dingding/钉钉使用手册.docx 45 | 46 | 47 | 2.. 要使用钉钉比较高级的功能 如回调 则 要自己探索了。上传的时候,一些功能已经不能用了(懒得修正了。后面的修改导致的。简单功能没问题。仅供参考) 48 | 49 | 3.加密部分代码来源于 https://github.com/zgs225/dingtalk_crypto 经过部分修改。 50 | 51 | 4.注意事项,钉钉注册回调事件时总会报出 返回字符串 success。 52 | 深究其原因,就发现是odoo json请求自动把返回的内容进行包装加上一层 "id": 622213306, "jsonrpc": "2.0", "result": 53 | ```json 54 | {"id": 622213306, "jsonrpc": "2.0", "result": [{"type": "ir.actions.server", "link_field_id": false, "name": "\u5b9a\u65f6\u83b7\u53d6\u6700\u65b0\u4ea7\u54c1\u4fe1\u606f", "active": false, "numbercall": -1, "channel_ids": [], "interval_number": 10, "model_id": [113, "\u8bfb\u53d6\u4eac\u4e1c\u7684\u4ea7\u54c1\u5206\u7c7b\u8bb0\u5f55\u4e0b\u6765\uff0c\u7136\u540e\u8fdb\u884c\u548c\u4ea7\u54c1\u7684\u5173\u8054\uff0c\u5206\u7c7bID\u548c\u4eac\u4e1c\u4e00\u81f4"], "doall": false, "model_name": "jd.category", "id": 15, "fields_lines": [], "priority": 8, "child_ids": [], "interval_type": "minutes", "template_id": false, "crud_model_id": false, "crud_model_name": false, "nextcall": "2018-03-15 02:56:20", "code": "model.all_search_and_write_new_info('all')", "display_name": "\u5b9a\u65f6\u83b7\u53d6\u6700\u65b0\u4ea7\u54c1\u4fe1\u606f", "user_id": [1, "Administrator"], "state": "code", "partner_ids": [], "binding_model_id": false}]} 55 | ``` 56 | 这样的结果而钉钉不能识别odoo的这种格式所以 要继承下面方法 把这一层包装去掉。 (测试临时修改,如需正式环境使用请用继承方式) 直接替换掉系统中这个方法 57 | ```python 58 | 59 | def _json_response(self, result=None, error=None): 60 | response = { 61 | 'jsonrpc': '2.0', 62 | 'id': self.jsonrequest.get('id') 63 | } 64 | if result and isinstance(result, dict)and result.get('msg_signature'): 65 | mime = 'application/json' 66 | body = json.dumps(result) 67 | return Response( 68 | body, headers=[('Content-Type', mime), 69 | ('Content-Length', len(body))]) 70 | if error is not None: 71 | response['error'] = error 72 | if result is not None: 73 | response['result'] = result 74 | 75 | if self.jsonp: 76 | # If we use jsonp, that's mean we are called from another host 77 | # Some browser (IE and Safari) do no allow third party cookies 78 | # We need then to manage http sessions manually. 79 | response['session_id'] = self.session.sid 80 | mime = 'application/javascript' 81 | body = "%s(%s);" % (self.jsonp, json.dumps(response),) 82 | else: 83 | mime = 'application/json' 84 | body = json.dumps(response) 85 | 86 | return Response( 87 | body, headers=[('Content-Type', mime), 88 | ('Content-Length', len(body))]) 89 | 90 | ``` 91 | 92 | 93 | 附录:钉钉官方解决 字符串不匹配 问题 方法 集合 94 | ``` 95 | :字符串不匹配? 96 | • 1.在创建套件过程中、在使用套件回调url接收各种回调处理过程中,会遇到钉钉提示 “返回字符串不匹配”。 97 | • 2.仔细阅读文档,确认你是按照文档的要求返回的字符串,有的场景要求返回加密“success”,有的地方要返回加密的random对应的key值。 98 | • 3.确定你的url是不是需要登录的。 99 | • 4.确定你的url返回的值是json(注意不是返回json.toString),而不是jsonp格式。 100 | • 5.对于nodejs,要设置 setAutoPadding 为 false,在进行 PKCS7 补全。 101 | • 6.终极大招:使用postman,既然你能够收到消息并返回,那么请你用log打印出你接接收的参数并贴在postman中。如图: 102 | 103 | ``` 104 | 105 | 106 | -------------------------------------------------------------------------------- /dingding/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbert-yuan/odoo_dingding/9711a27b5e7308093a90507617e90e0b6a5d0c8f/dingding/.DS_Store -------------------------------------------------------------------------------- /dingding/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from . import ding_model 3 | from . import ding_api 4 | from . import controller 5 | from . import dingtalk_crypto 6 | -------------------------------------------------------------------------------- /dingding/__manifest__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | { 3 | 'name': 'dingding', 4 | 'author': 'gilbert(静静)', 5 | 'website': '', 6 | 'summary': 'odoo中钉钉的嵌入使用', 7 | 'category': 'dingding', 8 | 'sequence': 11, 9 | 'description': '''''', 10 | 'depends': ['web'], 11 | 'data': 12 | ['views/ding_model.xml', 13 | 'views/res_users.xml', 14 | ], 15 | 'application': True, 16 | } 17 | -------------------------------------------------------------------------------- /dingding/controller/__init__.py: -------------------------------------------------------------------------------- 1 | from . import page_controller 2 | -------------------------------------------------------------------------------- /dingding/controller/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbert-yuan/odoo_dingding/9711a27b5e7308093a90507617e90e0b6a5d0c8f/dingding/controller/__init__.pyc -------------------------------------------------------------------------------- /dingding/controller/page_controller.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import http, api 3 | from odoo.http import HttpRequest, request 4 | from odoo.addons.dingding.dingtalk_crypto.crypto import DingTalkCrypto 5 | import os 6 | import sys 7 | import jinja2 8 | import threading 9 | import odoo 10 | import time 11 | import werkzeug 12 | from urllib import parse 13 | from odoo.tools.safe_eval import safe_eval 14 | from odoo.addons.dingding.ding_api import Dingtalk 15 | import requests 16 | from odoo.addons.web.controllers.main import Home 17 | 18 | 19 | if hasattr(sys, 'frozen'): 20 | # When running on compiled windows binary, we don't have access to package loader. 21 | path = os.path.realpath(os.path.join(os.path.dirname(__file__), '..', 'html')) 22 | loader = jinja2.FileSystemLoader(path) 23 | else: 24 | loader = jinja2.PackageLoader('odoo.addons.dingding.controller', "html") 25 | env = jinja2.Environment('<%', '%>', '${', '}', '%', loader=loader, autoescape=True) 26 | 27 | 28 | class DingDingLogin(Home): 29 | 30 | @http.route() 31 | def web_login(self, *args, **kw): 32 | registry = odoo.registry(request.db) 33 | 34 | with registry.cursor() as cr: 35 | env = odoo.api.Environment(cr, odoo.SUPERUSER_ID, {}) 36 | appid = env.ref('dingding.ding_ding_xml') 37 | host_url = 'https://oapi.dingtalk.com/connect/oauth2/sns_authorize' 38 | redirect_uri = 'http://jdvop.tunnel.800890.com/dingding/login' 39 | appid = 'dingoaosarn9kswmozjldq' 40 | origin_url = ("{host_url}?appid={appid}&response_type=code&scope=snsapi_login" 41 | "&state=STATE&redirect_uri={redirect_uri}".format(host_url=host_url, 42 | appid=appid, 43 | redirect_uri=redirect_uri)) 44 | url = parse.quote(origin_url, 'utf-8') 45 | request.params.setdefault('redirect_url_quote', url) 46 | request.params.setdefault('origin_url', origin_url) 47 | 48 | response = super(DingDingLogin, self).web_login(*args, **kw) 49 | return response 50 | 51 | 52 | class PageShow(http.Controller): 53 | def __init__(self): 54 | self.login_user = False 55 | self.login_session = False 56 | db_name = request.db 57 | self.threaded_token = threading.Thread(target=self.create_agent_token, args=(db_name,)) # 开启一个新的线程专门来跑redis 消费者 58 | self.threaded_token.setDaemon(True) 59 | self.threaded_token.start() 60 | 61 | def create_agent_token(self, db_name): 62 | # redis 生产者消费者模式中的消费者部分的代码 63 | registry = odoo.registry(db_name) 64 | with api.Environment.manage(), registry.cursor() as cr: 65 | env = api.Environment(cr, odoo.SUPERUSER_ID, {}) 66 | expired_in = 10 67 | while True: 68 | ding_config_row = env.ref('dingding.ding_ding_xml') 69 | if ding_config_row: 70 | if float(ding_config_row.expired_in or 0) <= time.time(): 71 | ding_obj = Dingtalk(corpid=ding_config_row.corpid, corpsecret=ding_config_row.corpsecret) 72 | token_dcit = ding_obj.get_token() 73 | if token_dcit.get('errcode') == 0 and token_dcit.get('access_token'): 74 | ding_config_row.token = token_dcit.get('access_token') 75 | ding_config_row.expired_in = int(token_dcit.get('expired_in')) 76 | env.cr.commit() 77 | else: 78 | expired_in = int(float(ding_config_row.expired_in) - time.time()) 79 | for app in ding_config_row.app_ids: 80 | if float(app.expired_in or 0) <= time.time(): 81 | ding_obj = Dingtalk(appkey=app.agent_id, appsecret=app.app_secret) 82 | token_dcit = ding_obj.app_get_token() 83 | if token_dcit.get('errcode') == 0 and token_dcit.get('access_token'): 84 | app.token = token_dcit.get('access_token') 85 | app.expired_in = int(token_dcit.get('expired_in')) 86 | env.cr.commit() 87 | else: 88 | expired_in = min(int(float(ding_config_row.expired_in) - time.time()), expired_in) 89 | time.sleep(expired_in) 90 | 91 | @http.route('/dingding/firstpage', auth='none', type="http", csrf=False) 92 | def dingding_pulling(self, **args): 93 | template = env.get_template("apps.html") 94 | corpid, corpsecret, agent_id, token_dict = request.env['ding.ding'].sudo().get_ding_common_message() 95 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, agent_id=agent_id, token=token_dict) 96 | signature, timestamp, nonceStr = ding_obj.get_js_api_params(str(request.httprequest.base_url), '1234') 97 | return template.render({ 98 | 'corpId': corpid, 99 | 'timeStamp': timestamp, 100 | 'agentId': agent_id, 101 | 'nonceStr': nonceStr, 102 | 'accessToken': token_dict.get("access_token"), 103 | 'title': u'钉钉测试', 104 | 'signature': signature, 105 | }) 106 | 107 | @http.route('/dingding/getdingdingconfig', auth='none', type="json", csrf=False) 108 | def getdingdingconfig(self, **args): 109 | corpid, corpsecret, agent_id, token_dict = request.env['ding.ding'].sudo().get_ding_common_message() 110 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, agent_id=agent_id, token=token_dict) 111 | signature, timestamp, nonceStr = ding_obj.get_js_api_params(str(request.httprequest.base_url), '1234') 112 | return { 113 | 'corpId': corpid, 114 | 'timeStamp': timestamp, 115 | 'agentId': agent_id, 116 | 'nonceStr': nonceStr, 117 | 'accessToken': token_dict.get("access_token"), 118 | 'title': u'钉钉测试', 119 | 'signature': signature, 120 | # 'jsApiList': jsApiList 121 | } 122 | 123 | @http.route('/get_user_info', auth='none', type="http", csrf=False) 124 | def dingding_get_user_info(self, **args): 125 | res = requests.get('https://oapi.dingtalk.com/user/getuserinfo', params=args) 126 | try: 127 | dingding_userid = res.json()['userid'] 128 | dinguser_row = request.env['ding.user'].sudo().search([('ding_id', '=', dingding_userid)]) 129 | if dinguser_row: 130 | uid = request.session.authenticate(request.session.db, dinguser_row.ding_user_id.login, 131 | dinguser_row.ding_user_id.oauth_access_token) 132 | return request.make_response(res.json()) 133 | except: 134 | return 'error' 135 | 136 | @http.route('/dingding/call_back_url', auth='none', type="json", csrf=False) 137 | def dingding_call_back(self, **args): 138 | ding_rows = request.env['ding.ding'].sudo().search([]) 139 | dingcrypto = DingTalkCrypto(ding_rows[0].aes_key1, str(ding_rows[0].random_token), str(ding_rows[0].corpid)) 140 | rand_str, length, msg, key = dingcrypto.decrypt(request.jsonrequest.get('encrypt')) 141 | if safe_eval(msg).get('EventType') != 'check_url': 142 | ding_rows.handler_map().get(safe_eval(msg).get('EventType'), None)(safe_eval(msg)) 143 | signature_get, timestamp_get, nonce_get = request.httprequest.args.get('signature'),\ 144 | request.httprequest.args.get('timestamp'), request.httprequest.args.get('nonce') 145 | 146 | dingcrypto = DingTalkCrypto(ding_rows[0].aes_key1, str(ding_rows[0].random_token), str(ding_rows[0].corpid)) 147 | encrypt = dingcrypto.encrypt("success") 148 | signature, timestamp, nonce = dingcrypto.sign(encrypt, timestamp_get, nonce_get) 149 | script_response = { 150 | 'msg_signature': signature, 151 | 'timeStamp': timestamp_get, 152 | 'nonce': nonce_get, 153 | 'encrypt': encrypt 154 | } 155 | return script_response 156 | 157 | @http.route('/dingding/login', auth='none', type="http", csrf=False) 158 | def dingding_login(self, **kwargs): 159 | for app_agent in request.env['app.agent'].sudo().search([]): 160 | ding_obj = Dingtalk(token={'access_token': app_agent.token}) 161 | persistent_code_dict = ding_obj.get_persistent_code(kwargs.get('code')) 162 | sns_token_dict = ding_obj.get_sns_token(persistent_code_dict.get('openid'), 163 | persistent_code_dict.get('persistent_code')) 164 | user_info_dict = ding_obj.get_user_info_by_sns_token(sns_token_dict.get('sns_token')) 165 | for user in request.env['ding.user'].sudo().search([('unionid', '=', user_info_dict.get('user_info', {}).get('unionid'))]): 166 | request.session.authenticate(request.session.db, user.ding_user_id.login, user.ding_user_id.oauth_access_token) 167 | return http.local_redirect('/web/') 168 | return werkzeug.utils.redirect('/web') 169 | 170 | -------------------------------------------------------------------------------- /dingding/controller/page_controller.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbert-yuan/odoo_dingding/9711a27b5e7308093a90507617e90e0b6a5d0c8f/dingding/controller/page_controller.pyc -------------------------------------------------------------------------------- /dingding/ding_api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding:utf-8 -*- 3 | # __author__ = '懒懒的天空' 4 | 5 | import requests 6 | import time 7 | import json 8 | import datetime 9 | from odoo.exceptions import UserError 10 | import hashlib 11 | import urllib 12 | import simplejson 13 | import functools 14 | # 这个设置可以去除 urllib3的不必要的 warning 15 | requests.packages.urllib3.disable_warnings() 16 | 17 | DEFAULT_SERVER_DATE_FORMAT = "%Y-%m-%d" 18 | DEFAULT_SERVER_TIME_FORMAT = "%H:%M:%S" 19 | DEFAULT_SERVER_DATETIME_FORMAT = "%s %s" % ( 20 | DEFAULT_SERVER_DATE_FORMAT, 21 | DEFAULT_SERVER_TIME_FORMAT) 22 | 23 | """ 24 | 单例模式 的其中一种写法 25 | """ 26 | 27 | 28 | class Singleton(object): 29 | def __init__(cls, name, bases, dict): 30 | super(Singleton, cls).__init__(name, bases, dict) 31 | cls._instance = None 32 | 33 | def __call__(cls, *args, **kw): 34 | if cls._instance is None: 35 | cls._instance = super(Singleton, cls).__call__(*args, **kw) 36 | return cls._instance 37 | 38 | # 所有的回调事件 39 | # 'user_add_org', 'user_modify_org', 'user_leave_org','org_admin_add', 'org_admin_remove', 'org_dept_create', 40 | # 'org_dept_modify', 'org_dept_remove', 'org_remove','label_user_change', 'label_conf_add', 'label_conf_modify', 41 | # 'label_conf_del','org_change', 'chat_add_member', 'chat_remove_member', 'chat_quit', 'chat_update_owner', 42 | # 'chat_update_title', 'chat_disband', 'chat_disband_microapp','check_in','bpms_task_change','bpms_instance_change' 43 | 44 | class Dingtalk(Singleton): 45 | def __init__(self, corpid=None, corpsecret=None, 46 | agent_id=None, token={}, appkey=None, appsecret=None): # 初始化的时候需要获取corpid和corpsecret, 47 | # 需要从管理后台获取 , corpid, corpsecret, agent_id 48 | self.__params = { 49 | 'corpid': corpid, 50 | 'corpsecret': corpsecret, 51 | } 52 | self.__app_params = { 53 | 'appid': appkey, 54 | 'appsecret': appsecret, 55 | } 56 | self.token_dict = { 57 | 'access_token': token.get('access_token') 58 | } 59 | self._header = {'Content-Type': 'application/json'} # 全局固定用的 请求的 header 个别的不一样要单独写 60 | self._agent_id = agent_id, 61 | 62 | self._url_jsapi_ticket = 'https://oapi.dingtalk.com/get_jsapi_ticket' 63 | self.url_get_token = 'https://oapi.dingtalk.com/gettoken' 64 | self.url_get_app_token = 'https://oapi.dingtalk.com/sns/gettoken' 65 | self.get_sso_token = 'https://oapi.dingtalk.com/sso/gettoken' 66 | self.url_get_sns_token = 'https://oapi.dingtalk.com/sns/get_sns_token' 67 | self.url_get_dept_list = 'https://oapi.dingtalk.com/department/list' 68 | self.url_get_dept_detail = 'https://oapi.dingtalk.com/department/get' 69 | self.url_create_dept = 'https://oapi.dingtalk.com/department/create' 70 | self.url_delete_dept = 'https://oapi.dingtalk.com/department/delete' 71 | self.url_update_dept = 'https://oapi.dingtalk.com/department/update' 72 | self.url_get_user_id_by_unionid = 'https://oapi.dingtalk.com/user/getUseridByUnionid' 73 | self.url_get_user_detail = 'https://oapi.dingtalk.com/user/get' 74 | self.url_send_message = 'https://oapi.dingtalk.com/message/send_to_conversation' 75 | self.url_send = 'https://eco.taobao.com/router/rest' 76 | self.url_create_user = 'https://oapi.dingtalk.com/user/create' 77 | self.url_update_user = 'https://oapi.dingtalk.com/user/update' 78 | self.url_user_list = 'https://oapi.dingtalk.com/user/list' 79 | self.url_get_user_count = 'https://oapi.dingtalk.com/user/get_org_user_count' 80 | self.url_get_persistent_code = "https://oapi.dingtalk.com/sns/get_persistent_code" 81 | self.url_get_user_info_by_sns_token = 'https://oapi.dingtalk.com/sns/getuserinfo' 82 | self.url_delete_user = 'https://oapi.dingtalk.com/user/delete' 83 | self.url_register_call_back_interface = "https://oapi.dingtalk.com/call_back/register_call_back" 84 | self.url_update_call_back_interface = "https://oapi.dingtalk.com/call_back/update_call_back" 85 | self.url_delete_call_back_interface = "https://oapi.dingtalk.com/call_back/delete_call_back" 86 | self.url_checkout_call_back_interface = "https://oapi.dingtalk.com/call_back/get_call_back" 87 | self.url_get_call_fail_record = 'https://oapi.dingtalk.com/call_back/get_call_back_failed_result' 88 | 89 | self.approver_common_url = 'https://eco.taobao.com/router/rest' 90 | self.approver_common_header = {'Content-Type': 'application/x-www-form-urlencoded', 91 | 'charset': 'utf-8'} 92 | self.method_approver_create_processinstance = 'dingtalk.smartwork.bpms.processinstance.create' 93 | 94 | def get_call_fail_record(self): 95 | res = requests.get(self.url_get_call_fail_record, 96 | headers=self._header, 97 | params=self.token_dict) 98 | try: 99 | return res.json() 100 | except: 101 | self.__raise_error(res) 102 | 103 | def get_user_info_by_sns_token(self, sns_token): 104 | params = { 105 | 'sns_token': sns_token 106 | } 107 | params.update(self.token_dict) 108 | res = requests.get(self.url_get_user_info_by_sns_token, 109 | headers=self._header, 110 | params=params) 111 | try: 112 | return res.json() 113 | except: 114 | self.__raise_error(res) 115 | 116 | def get_persistent_code(self, code): 117 | params = { 118 | 'tmp_auth_code': code 119 | } 120 | res = requests.post(self.url_get_persistent_code, 121 | headers=self._header, 122 | params=self.token_dict, 123 | data=json.dumps(params)) 124 | try: 125 | return res.json() 126 | except: 127 | self.__raise_error(res) 128 | 129 | def get_sns_token(self, openid, persistent_code): 130 | params = { 131 | 'openid': openid, 132 | 'persistent_code': persistent_code, 133 | } 134 | res = requests.post(self.url_get_sns_token, 135 | headers=self._header, 136 | params=self.token_dict, 137 | data=json.dumps(params)) 138 | try: 139 | return res.json() 140 | except: 141 | self.__raise_error(res) 142 | 143 | def delete_call_back_interface(self): 144 | """ 145 | 删除回调接口 链接 :return: 146 | """ 147 | res = requests.get(self.url_delete_call_back_interface, 148 | headers=self._header, 149 | params=self.token_dict) 150 | try: 151 | return res.json()['errcode'] 152 | except: 153 | self.__raise_error(res) 154 | 155 | def update_call_back_interface(self, token, aes_key, url, call_back_tags): 156 | """ 157 | 更新回调接口 链接 :return: 158 | """ 159 | data = { 160 | "call_back_tag": call_back_tags, 161 | "token": token, 162 | "aes_key": aes_key, 163 | "url": url 164 | } 165 | res = requests.post(self.url_update_call_back_interface, 166 | headers=self._header, 167 | params=self.token_dict, 168 | data=json.dumps(data)) 169 | try: 170 | return res.json()['errcode'] 171 | except: 172 | self.__raise_error(res) 173 | 174 | def register_call_back_interface(self, token, aes_key, url, call_back_tags): 175 | """ 176 | 注册回调接口 177 | :param token: 在钉钉模型上的 token 随机填写 178 | :param aes_key: 在钉钉模型上的 aes_key 随机生成的 43位 aes_key 179 | :param url: ‘填写要回调的URL 地址’ 180 | :param call_back_tags: ‘填写要回调的 事件’ 181 | :return: 182 | """ 183 | data = { 184 | "call_back_tag": call_back_tags, 185 | "token": token, 186 | "aes_key": aes_key, 187 | "url": url 188 | } 189 | res = requests.post(self.url_register_call_back_interface, 190 | headers=self._header, 191 | params=self.token_dict, 192 | data=json.dumps(data)) 193 | try: 194 | return res.json()['errcode'] 195 | except: 196 | self.__raise_error(res) 197 | 198 | def checkout_call_back_interface(self): 199 | """ 200 | 检查是否回调事件注册成功 201 | :return: 202 | """ 203 | res = requests.get(self.url_checkout_call_back_interface, 204 | headers=self._header, 205 | params=self.token_dict) 206 | try: 207 | return res.json()['errcode'] 208 | except: 209 | self.__raise_error(res) 210 | 211 | def __raise_error(self, res): 212 | """ 213 | 弹出事件返回的报错信息 214 | :param res: 215 | :return: 216 | """ 217 | raise UserError(u'错误代码: %s,详细错误信息: %s' % (res.json()['errcode'], res.json()['errmsg'])) 218 | 219 | def app_get_token(self): 220 | """ 221 | 获取大部分时间的token(有的事件链接还要单独获取不同的token) 222 | :return: token 并取得token 获取token的时间 223 | """ 224 | res = requests.get(self.url_get_app_token, headers=self._header, params=self.__app_params) 225 | try: 226 | token_vals = res.json() 227 | token_vals.update({'expired_in': (time.time() + 7200)}) 228 | return token_vals 229 | except: 230 | self.__raise_error(res) 231 | 232 | def get_token(self): 233 | """ 234 | 获取大部分时间的token(有的事件链接还要单独获取不同的token) 235 | :return: token 并取得token 获取token的时间 236 | """ 237 | res = requests.get(self.url_get_token, headers=self._header, params=self.__params) 238 | try: 239 | token_vals = res.json() 240 | token_vals.update({'expired_in': (time.time() + 7200)}) 241 | return token_vals 242 | except: 243 | self.__raise_error(res) 244 | 245 | def get_common_param(self, method): 246 | date_now = datetime.datetime.now() 247 | common_param = dict(v='2.0', 248 | format='json', 249 | session=(self.token_dict).get('access_token'), 250 | method=method, 251 | partner_id='apidoc', 252 | timestamp=date_now.strftime('%Y-%m-%d %H:%M:%S'), 253 | ) 254 | return common_param 255 | 256 | def delete_user(self, user_id): 257 | """ 258 | 删除用户 259 | :param user_id: 260 | :return: 261 | """ 262 | params = self.token_dict 263 | params.update({'userid': user_id}) 264 | res = requests.get(self.url_delete_user, params=params) 265 | try: 266 | return res.json()['errcode'] 267 | except: 268 | self.__raise_error(res) 269 | 270 | def create_user(self, user_vals): 271 | """ 272 | 新建用户 273 | :param user_vals: 274 | :return: 275 | """ 276 | res = requests.post(self.url_create_user, 277 | headers=self._header, 278 | params=self.token_dict, 279 | data=json.dumps(self.remove_False_vals(user_vals))) 280 | try: 281 | return res.json()['userid'] 282 | except: 283 | self.__raise_error(res) 284 | 285 | def update_user(self, user_vals): 286 | """ 287 | 更新用户信息 288 | :param user_vals: 289 | :return: 290 | """ 291 | res = requests.post(self.url_update_user, 292 | params=self.token_dict, 293 | data=json.dumps(user_vals)) 294 | try: 295 | return res.json()['errmsg'] 296 | except: 297 | self.__raise_error(res) 298 | 299 | def update_dept(self, dept_vals): 300 | """ 301 | 更新部门的信息 302 | :param dept_vals: 303 | :return: 304 | """ 305 | res = requests.post(self.url_update_dept, 306 | params=self.token_dict, 307 | data=json.dumps(dept_vals)) 308 | try: 309 | return res.json()['errmsg'] 310 | except: 311 | self.__raise_error(res) 312 | 313 | def get_dept_list(self): 314 | """ 315 | 获取部门列表 316 | :return: 317 | """ 318 | res = requests.get(self.url_get_dept_list, 319 | params=self.token_dict) 320 | try: 321 | return res.json()['department'] 322 | except: 323 | self.__raise_error(res) 324 | 325 | def get_depatment_user_list(self, department_id): 326 | """ 327 | 获取部门的 用户列表 328 | :param department_id: 329 | :return: 330 | """ 331 | params = self.token_dict 332 | params.update({'department_id': department_id}) 333 | res = requests.get(self.url_user_list, 334 | params=params) 335 | try: 336 | return res.json()['userlist'] 337 | except: 338 | self.__raise_error(res) 339 | 340 | def get_dept_detail(self, dept_id): 341 | """ 342 | 获取部门的详细情况 343 | :param dept_id: 344 | :return: 345 | """ 346 | params = self.token_dict 347 | params.update({'id': dept_id}) 348 | res = requests.get(self.url_get_dept_detail, 349 | params=params) 350 | try: 351 | return res.json() 352 | except: 353 | self.__raise_error(res) 354 | 355 | def create_dept(self, name, parentid, orderid, createdeptgroup=True): 356 | """ 357 | 创建新的部门 358 | :param name: 359 | :param parentid: 360 | :param orderid: 361 | :param createdeptgroup: 362 | :return: 363 | """ 364 | payload = { 365 | 'name': name, 366 | 'parentid': parentid or '1', 367 | 'orderid': orderid, 368 | 'createDeptGroup': createdeptgroup, 369 | } 370 | res = requests.post(self.url_create_dept, 371 | headers=self._header, 372 | params=self.token_dict, 373 | data=json.dumps(payload)) 374 | try: 375 | return res.json()['id'] 376 | except: 377 | self.__raise_error(res) 378 | 379 | def delete_dept(self, dept_id): 380 | """ 381 | 删除部门 382 | :param dept_id: 383 | :return: 384 | """ 385 | params = self.token_dict 386 | params.update({'id': dept_id}) 387 | res = requests.get(self.url_delete_dept, params=params) 388 | try: 389 | return res.json()['errcode'] 390 | except: 391 | self.__raise_error(res) 392 | 393 | def get_userid_by_unionid(self, unionid): 394 | """ 395 | 获取用户详细情况 通过 unionid 396 | :param unionid: 397 | :return: 398 | """ 399 | params = self.token_dict 400 | params.update({'unionid': unionid}) 401 | res = requests.get(self.url_get_user_id_by_unionid, params=params) 402 | try: 403 | return res.json()['userid'] 404 | except: 405 | self.__raise_error(res) 406 | 407 | def get_js_api_ticket(self): 408 | """ 409 | 获取 jsapi 的访问 票据 410 | :return: 411 | """ 412 | res = requests.get(self._url_jsapi_ticket, params=self.token_dict) 413 | try: 414 | return res.json()['ticket'] 415 | except: 416 | self.__raise_error(res) 417 | 418 | def cmp(self, val_one, val_two): 419 | return_val = 0 420 | if val_one > val_two: 421 | return_val = 1 422 | elif val_one < val_two: 423 | return_val = -1 424 | return return_val 425 | 426 | def get_signature(self, vals={}): 427 | """ 428 | 对jsapi 参数进行处理获取 对应参数的 signature 429 | :param vals: 430 | :return: 431 | """ 432 | sorted_vals = sorted(vals.items(), lambda x, y: self.cmp(x[1], y[1])) 433 | url_vals = urllib.urlencode(sorted_vals) 434 | signature = hashlib.sha1(url_vals).hexdigest() # sha 加密 435 | return signature 436 | 437 | def get_js_api_params(self, url, nonceStr): 438 | """ 439 | 处理 数据 去的 jsapi 需要的 各种参数 440 | :param url: 441 | :param nonceStr: 442 | :return: 返回各种需要的参数 在 jsapi 中 443 | """ 444 | jsapi_ticket = self.get_js_api_ticket() 445 | timestamp = int(time.time()) 446 | signature_vals = { 447 | 'noncestr': nonceStr, 448 | 'jsapi_ticket': jsapi_ticket, 449 | 'url': url, 450 | 'timestamp': timestamp, 451 | } 452 | signature = self.get_signature(signature_vals) 453 | try: 454 | return signature, timestamp, nonceStr 455 | except: 456 | self.__raise_error({"error": u'错误!'}) 457 | 458 | def get_user_detail(self, userid): 459 | params = self.token_dict 460 | params.update({'userid': userid}) 461 | res = requests.get(self.url_get_user_detail, params=params) 462 | try: 463 | return res.json() 464 | except: 465 | self.__raise_error(res) 466 | 467 | def remove_False_vals(self, dict_vals={}): 468 | """ 469 | 取出没有修改的地方的内容 470 | :param dict_vals: 471 | :return: 472 | """ 473 | keys = [key for key, vals in dict_vals.items() if not dict_vals.get(key)] 474 | for key in keys: 475 | del dict_vals[key] 476 | return dict_vals 477 | 478 | def send_message(self, messages, userid_list='', dept_id_list='', 479 | msgtype='text', to_all_user='false'): 480 | """ 481 | 发送消息 482 | :param messages: 消息体内容 483 | :param userid_list: 接受消息的用户列表 484 | :param dept_id_list: 接受消息的部门列表 485 | :param msgtype: 消息的类型 486 | :param to_all_user: 是否发送给每一个人 487 | :return: 488 | """ 489 | payload = { 490 | 'agent_id': self._agent_id[0], 491 | "msgtype": msgtype, 492 | 'userid_list': userid_list, 493 | 'dept_id_list': dept_id_list, 494 | 'to_all_user': to_all_user, 495 | "msgcontent": json.dumps(messages) 496 | } 497 | params = {'v': 2.0, 498 | 'method': 'dingtalk.corp.message.corpconversation.asyncsend', 499 | 'format': 'json', 500 | 'session': (self.token_dict).get('access_token'), 501 | 'timestamp': datetime.datetime.now()} 502 | 503 | res = requests.post(self.url_send, 504 | headers={'Content-Type': 505 | 'application/x-www-form-urlencoded;\charset=utf-8'}, 506 | params=params, 507 | data=self.remove_False_vals(payload)) 508 | try: 509 | return res.json() 510 | except: 511 | self.__raise_error(res) 512 | 513 | def get_user_count(self, only_active=0): 514 | """ 515 | 获取 用户的 516 | :param only_active: 517 | :return: 518 | """ 519 | params = self.token_dict 520 | params.update({'onlyActive': only_active}) 521 | res = requests.get(self.url_get_user_count, params=params) 522 | try: 523 | return res.json()['count'] 524 | except: 525 | self.__raise_error(res) 526 | 527 | def send_oa_message(self, message, userid_list='', dept_id_list='', to_all_user='false'): 528 | """ 529 | 发送 固定格式的消息的一个格式的设定 530 | :param message: 531 | :param userid_list: 532 | :param dept_id_list: 533 | :param to_all_user: 534 | :return: 535 | """ 536 | message = {"message_url": message.get("message_url"), 537 | "head": {"bgcolor": message.get('bgcolor'), 538 | "text": message.get('head')}, 539 | "body": {"title": message.get('title'), 540 | "form": message.get('form'), 541 | # [{"key": "姓名:","value": "张三"}, {"key": "爱好:","value": "打球、听音乐"}] 542 | "rich": message.get('rich'), 543 | # {"num": "15.6","unit": "元"}, 544 | "content": message.get('content'), 545 | # "大段文本大段文本大段文本大段文本大段文本大段文本大段文本大段文本大段文本大段文本大段文本大段文本", 546 | "image": message.get('image'), 547 | # "@lADOADmaWMzazQKA", 548 | "file_count": message.get('file_count'), 549 | # "3", 550 | "author": message.get('author'), 551 | # "李四 "} 552 | }} 553 | self.send_message(message, 554 | userid_list=userid_list, 555 | dept_id_list=dept_id_list, 556 | msgtype='oa', 557 | to_all_user=to_all_user) 558 | return True 559 | 560 | def send_text_message(self, message, userid_list, dept_id_list): 561 | """ 562 | 发送普通消息的 简化参数 563 | :param message: 消息内容 564 | :param userid_list: 发送用户 列表 565 | :param dept_id_list: 发送部门的列表 566 | :return: 567 | """ 568 | self.send_message({"content": message}, 569 | userid_list=userid_list, 570 | dept_id_list=dept_id_list, 571 | msgtype='text', ) 572 | return True 573 | 574 | def create_new_approver(self, vals): 575 | vals.update(self.get_common_param(self.method_approver_create_processinstance)) 576 | res = requests.post(self.approver_common_url, 577 | headers=self.approver_common_header, 578 | params=vals) 579 | all_response = res.json()['dingtalk_smartwork_bpms_processinstance_create_response'] 580 | if all_response.get('result').get('is_success'): 581 | return True 582 | else: 583 | raise UserError(all_response.get('result').get('error_msg')) 584 | 585 | 586 | jsApiList = ['device.notification.alert', 587 | 'device.notification.confirm', 588 | 'device.notification.prompt', 589 | 'device.notification.vibrate', 590 | 'device.accelerometer.watchShake', 591 | 'device.accelerometer.clearShake', 592 | 'device.notification.toast', 593 | 'device.notification.actionSheet', 594 | 'device.notification.showPreloader', 595 | 'device.notification.hidePreloader', 596 | 'biz.navigation.setLeft', 597 | 'biz.navigation.setRight', 598 | 'biz.navigation.setTitle', 599 | 'device.connection.getNetworkType', 600 | 'biz.util.openLink', 601 | 'biz.util.datepicker', 602 | 'biz.util.timepicker', 603 | 'biz.util.datetimepicker', 604 | 'biz.navigation.goBack', 605 | 'biz.navigation.close', 606 | 'biz.navigation.setMenu', 607 | 'biz.navigation.replace', 608 | 'biz.util.previewImage', 609 | 'biz.util.chosen', 610 | 'ui.input.plain', 611 | 'ui.progressBar.setColors', 612 | 'ui.pullToRefresh.enable', 613 | 'ui.pullToRefresh.disable', 614 | 'ui.pullToRefresh.stop', 615 | 'ui.webViewBounce.disable', 616 | 'ui.webViewBounce.enable', 617 | 'runtime.permission.requestAuthCode', 618 | 'device.notification.modal', 619 | 'biz.util.scan', 620 | 'biz.navigation.setIcon', 621 | 'ui.nav.preload', 622 | 'ui.nav.go', 623 | 'ui.nav.recycle', 624 | 'ui.nav.getCurrentId', 625 | 'ui.nav.close', 626 | 'ui.nav.backTo', 627 | 'ui.nav.push', 628 | 'ui.nav.pop', 629 | 'ui.nav.quit', 630 | 'device.base.getSettings', 631 | 'device.nfc.nfcRead', 632 | 'util.domainStorage.setItem', 633 | 'util.domainStorage.getItem', 634 | 'util.domainStorage.removeItem', 635 | 'service.request.httpOverLwp', 636 | 'device.geolocation.get', 637 | 'device.base.getUUID', 638 | 'device.base.getInterface', 639 | 'device.launcher.checkInstalledApps', 640 | 'device.launcher.launchApp', 641 | 'biz.util.open', 642 | 'biz.util.share', 643 | 'biz.contact.choose', 644 | 'biz.user.get', 645 | 'biz.util.uploadImage', 646 | 'biz.ding.post', 647 | 'biz.telephone.call', 648 | 'biz.telephone.showCallMenu', 649 | 'biz.chat.chooseConversation', 650 | 'biz.contact.createGroup', 651 | 'biz.map.locate', 652 | 'biz.map.search', 653 | 'biz.map.view', 654 | 'device.geolocation.openGps', 655 | 'biz.util.uploadImageFromCamera', 656 | 'biz.customContact.multipleChoose', 657 | 'biz.customContact.choose', 658 | 'biz.contact.complexPicker', 659 | 'biz.contact.departmentsPicker', 660 | 'biz.contact.setRule', 661 | 'biz.contact.externalComplexPicker', 662 | 'biz.contact.externalEditForm', 663 | 'biz.chat.pickConversation', 664 | 'biz.chat.chooseConversationByCorpId', 665 | 'biz.chat.openSingleChat', 666 | 'biz.chat.toConversation', 667 | 'biz.cspace.saveFile', 668 | 'biz.cspace.preview', 669 | 'biz.cspace.chooseSpaceDir', 670 | 'biz.util.uploadAttachment', 671 | 'biz.clipboardData.setData', 672 | 'biz.intent.fetchData', 673 | 'biz.chat.locationChatMessage', 674 | 'device.audio.startRecord', 675 | 'device.audio.stopRecord', 676 | 'device.audio.onRecordEnd', 677 | 'device.audio.download', 678 | 'device.audio.play', 679 | 'device.audio.pause', 680 | 'device.audio.resume', 681 | 'device.audio.stop', 682 | 'device.audio.onPlayEnd', 683 | 'device.audio.translateVoice', 684 | 'biz.util.fetchImageData', 685 | 'biz.alipay.auth', 686 | 'biz.alipay.pay', 687 | 'device.nfc.nfcWrite', 688 | 'biz.util.encrypt', 689 | 'biz.util.decrypt', 690 | 'runtime.permission.requestOperateAuthCode', 691 | 'biz.util.scanCard', ] 692 | -------------------------------------------------------------------------------- /dingding/ding_api.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbert-yuan/odoo_dingding/9711a27b5e7308093a90507617e90e0b6a5d0c8f/dingding/ding_api.pyc -------------------------------------------------------------------------------- /dingding/ding_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from odoo import models, fields, api 3 | from odoo.addons.dingding.ding_api import Dingtalk 4 | from odoo.exceptions import UserError 5 | import random, simplejson 6 | 7 | 8 | ALLCHAR = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890' 9 | ALLCALLBACKTAG = ['user_add_org', 'user_modify_org', 'user_leave_org', 'org_admin_add', 'org_admin_remove', 10 | 'org_dept_create', 'org_dept_modify', 'org_dept_remove', 'org_remove', 'chat_add_member', 11 | 'chat_remove_member', 'chat_quit', 'chat_update_owner', 'chat_update_title', 'chat_disband', 12 | 'chat_disband_microapp', 'check_in', 'bpms_task_change', 'bpms_instance_change', 'label_user_change', 13 | 'label_conf_add', 'label_conf_modify', 'label_conf_del' 14 | ] 15 | 16 | class ding_ding(models.Model): 17 | _name = 'ding.ding' 18 | _description = u'钉钉账户主要信息设置' 19 | 20 | name = fields.Char(u'钉钉对象') 21 | #钉钉中的基本的配置 22 | corpid = fields.Char(u'钉钉corpid', required=True, help=u'由钉钉开放平台提供给开放应用的唯一标识') 23 | corpsecret = fields.Char(u'钉钉corpsecret', required=True) 24 | agent_ids = fields.One2many('ding.agent', 'ding_id', string='自建应用(程序主要用于发送消息)', required=True) 25 | app_ids = fields.One2many('app.agent', 'ding_id', string='用于扫码登陆', required=True) 26 | eagent_ids = fields.One2many('e.agent', 'ding_id', string='自建e 应用', 27 | help='钉钉新推出了E应用开发,E应用开发是一种全新的开发模式,通过简洁的前端语法写出Native级别的性能体验' 28 | ',支持iOS、安卓等多端(PC端暂不支持)部署。', required=True) 29 | # 保存token(和重置) 用的字段 30 | token = fields.Char(u'token', readonly=True) 31 | expired_in = fields.Char(u'过期时间', readonly=True) 32 | # 这一部分字段用于 钉钉审批对接 (钉钉自带应用)事件回调 所用到的字段 33 | aes_key1 = fields.Char(u'随机字符串', default=lambda self: ''.join(random.sample(ALLCHAR, 43)), readonly=True) 34 | random_token = fields.Char(u'随机token', default='01234565789') 35 | call_back_url = fields.Char(u'会调事件URL', default='http:odoo10.tunnel.800890.com/dingding/call_back_url') 36 | call_back_tags = fields.Char(u'回调事件', default=ALLCALLBACKTAG) 37 | is_ok_call_back_url = fields.Boolean(u'回调接口设置正常') 38 | # 钉钉中访问第三方台获取钉钉用户信息 所用到的设置, 及字段 39 | # 钉钉文档链接https://open-doc.dingtalk.com/docs/doc.htm?spm=a219a.7629140.0.0.USEUsY&treeId=168&articleId=104881&docType=1 40 | # https://oapi.dingtalk.com/connect/oauth2/sns_authorize?appid=APPID&response_type=code&scope=snsapi_login&state=STATE&redirect_uri=REDIRECT_URI 41 | # 钉钉中访问第三方平台 ,回调url 42 | appsecret = fields.Char(u'appsecret', help=u'由钉钉开放平台提供的密钥') 43 | sns_token = fields.Char(u'snstoken', help=u' 使用appid及appSecret访问如下接口, 获取accesstoken,此处获取的token有效期为2小时,\ 44 | 有效期内重复获取,返回相同值,并自动续期,如果在有效期外获取会获得新的token值,建议定时获取本token,不需要用户登录时再获取。') 45 | sns_token_expired_in = fields.Char(u'过期时间', readonly=True) 46 | 47 | 48 | def handler_map(self): 49 | if getattr(self, 'handlers', None): 50 | return self.handlers 51 | return { 52 | 'user_add_org': self.user_add_org, 53 | #'check_url': self.register_call_back_interface, 54 | 'user_modify_org': self.user_modify_org, 55 | 'user_leave_org': self.user_leave_org, 56 | 'org_admin_add': self.org_admin_add, 57 | 'org_admin_remove': self.org_admin_remove, 58 | 'org_dept_create': self.org_dept_create, 59 | 'org_dept_modify': self.org_dept_modify, 60 | 'org_dept_remove': self.org_dept_remove, 61 | 'org_remove': self.org_remove, 62 | 'label_user_change': self.label_user_change, 63 | 'label_conf_add': self.label_conf_add, 64 | 'label_conf_modify': self.label_conf_modify, 65 | 'label_conf_del': self.label_conf_del, 66 | 'org_change': self.org_change, 67 | 'chat_add_member': self.chat_add_member, 68 | 'chat_remove_member': self.chat_remove_member, 69 | 'chat_quit': self.chat_quit, 70 | 'chat_update_owner': self.chat_update_owner, 71 | 'chat_update_title': self.chat_update_title, 72 | 'chat_disband': self.chat_disband, 73 | 'chat_disband_microapp': self.chat_disband_microapp, 74 | 'check_in': self.check_in, 75 | 'bpms_task_change': self.bpms_task_change, 76 | 'bpms_instance_change': self.bpms_instance_change, 77 | } 78 | 79 | def chat_disband(self, msg): 80 | pass 81 | 82 | def chat_update_owner(self, msg): 83 | """更改群聊 所有者""" 84 | pass 85 | 86 | def chat_update_title(self, msg): 87 | """更新群聊 名称""" 88 | pass 89 | 90 | def chat_disband_microapp(self, msg): 91 | """解散群聊""" 92 | pass 93 | 94 | def chat_remove_member(self, msg): 95 | """删除群聊 中用户""" 96 | pass 97 | 98 | def chat_quit(self, msg): 99 | """用户推出群聊""" 100 | pass 101 | 102 | def org_change(self, msg): 103 | """ 企业信息发生变更""" 104 | pass 105 | 106 | def chat_add_member(self, msg): 107 | """群聊添加 成员""" 108 | pass 109 | 110 | def user_add_org(self, msg): 111 | """通讯录用户增加""" 112 | pass 113 | def user_modify_org(self, msg): 114 | """通讯录用户更改""" 115 | pass 116 | 117 | def user_leave_org(self, msg): 118 | """通讯录用户离职""" 119 | pass 120 | 121 | def org_admin_add(self, msg): 122 | """通讯录用户被设为管理员""" 123 | pass 124 | 125 | def org_admin_remove(self, msg): 126 | """通讯录用户被取消设置管理员""" 127 | pass 128 | def org_dept_create(self, msg): 129 | """ 通讯录企业部门创建""" 130 | pass 131 | 132 | def org_dept_modify(self, msg): 133 | """通讯录企业部门修改""" 134 | pass 135 | 136 | def org_dept_remove(self, msg): 137 | """通讯录企业部门删除""" 138 | pass 139 | 140 | def org_remove(self, msg): 141 | """企业被解散""" 142 | pass 143 | 144 | def label_user_change(self, msg): 145 | """员工角色信息发生变更""" 146 | pass 147 | 148 | def label_conf_add(self, msg): 149 | """增加角色或者角色组""" 150 | pass 151 | def label_conf_modify(self, msg): 152 | """修改角色或者角色组""" 153 | pass 154 | 155 | def label_conf_del(self, msg): 156 | """删除角色或者角色组""" 157 | pass 158 | 159 | def bpms_task_change(self, msg): 160 | """审批任务开始 审批任务结束 审批任务转交""" 161 | pass 162 | 163 | def bpms_instance_change(self, msg): 164 | """审批实例开始 审批实例结束|终止""" 165 | pass 166 | 167 | def check_in(self, msg): 168 | pass 169 | 170 | def create_new_approver(self): 171 | agent_row = self.env.ref("dingding.ding_agent_xml") 172 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message(agent_row.agent_id) 173 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 174 | vals = { 175 | 'agent_id': agent_id, 176 | 'process_code': 'PROC-TXEKLZ3V-EJKNZY8FRS05FSWKCBOW1-6CGS3J6J-R', 177 | 'originator_user_id': 'manager1461', 178 | 'dept_id': '44507267', 179 | 'approvers': 'manager1461', 180 | 'cc_list': 'manager1461', 181 | 'cc_position': 'START_FINISH', 182 | 'form_component_values': simplejson.dumps([{"name": u"采购备注", "value": u"买个西瓜 甜瓜"}, 183 | {'name': u'明细', "value": [[{"name": "产品", "value": "西瓜"}, 184 | {"name": "数量", "value": "10"}, 185 | {"name": "价格", "value": "100"}], 186 | [{"name": "产品", "value": "甜瓜"}, 187 | {"name": "数量", "value": "5"}, 188 | {"name": "价格", "value": "2"}, 189 | ] 190 | ] 191 | } 192 | ]) 193 | } 194 | return_vals = ding_obj.create_new_approver(vals) 195 | if return_vals == '0': 196 | return True 197 | else: 198 | print("error") 199 | 200 | def get_call_fail_record(self): 201 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 202 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 203 | return_vals = ding_obj.get_call_fail_record() 204 | raise UserError(str(return_vals)) 205 | 206 | def register_call_back_interface(self): 207 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 208 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 209 | return_vals = ding_obj.register_call_back_interface(str(self.random_token), 210 | str(self.aes_key1), 211 | self.call_back_url, 212 | eval(self.call_back_tags)) 213 | if return_vals == '0': 214 | return True 215 | else: 216 | print(return_vals) 217 | 218 | def delete_call_back_interface(self): 219 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 220 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 221 | return_vals = ding_obj.delete_call_back_interface() 222 | if return_vals == '0': 223 | return True 224 | else: 225 | print("error") 226 | 227 | def update_call_back_interface(self): 228 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 229 | ding_obj = Dingtalk(ccorpid=corpid, corpsecret=corpsecret, token=token_dict) 230 | return_vals = ding_obj.update_call_back_interface(str(self.random_token), 231 | str(self.aes_key1), 232 | self.call_back_url, 233 | eval(self.call_back_tags)) 234 | if return_vals == '0': 235 | return True 236 | else: 237 | print("error") 238 | 239 | def checkout_call_back_interface(self): 240 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 241 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 242 | return_vals = ding_obj.checkout_call_back_interface() 243 | if return_vals == 0: 244 | self.is_ok_call_back_url = True 245 | return True 246 | else: 247 | print("error") 248 | 249 | def get_ding_department(self): 250 | department_obj = self.env['ding.department'] 251 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 252 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 253 | department_dict = ding_obj.get_dept_list() 254 | if not department_dict: 255 | raise UserError(u'企业号中还没创建部门') 256 | cr = self.env.cr 257 | department_row_parent = False 258 | for department in department_dict: 259 | if department.get('parentid'): 260 | department_row_parent = department_obj.search([('department_id', '=', department.get('parentid'))]) 261 | if department.get('id') and department.get('name'): 262 | department_row = department_obj.search([('department_id', '=', department.get('id'))]) 263 | if not department_row: 264 | cr.execute("""INSERT INTO ding_department 265 | (department_id, name, parent_id) VALUES 266 | (%s,'%s', %s)""" % (department.get('id'), 267 | department.get('name'), 268 | department_row_parent 269 | and department_row_parent.id or 'NULL')) 270 | return True 271 | 272 | def get_ding_user(self, user): 273 | cr = self.env.cr 274 | if user.get('mobile'): 275 | department_row = self.env['ding.department'].search([("department_id", '=', 276 | (user.get('department')[ 277 | len(user.get('department')) - 1]))]) 278 | parnter_row = self.env['res.partner'].search([('mobile', '=', user.get('mobile'))]) 279 | if parnter_row: 280 | parnter_row.department_id = department_row.id 281 | if parnter_row.user_ids: 282 | parnter_row.user_ids[0].oauth_access_token = user.get('unionid') 283 | cr.execute("""INSERT INTO ding_user 284 | (department_id, name, work_place, ding_id ,mobile_num, email, 285 | position, ding_user_id, jobnumber,open_id, ishide, active, unionid, dingding_id) VALUES 286 | (%s,'%s', '%s', '%s','%s', '%s', '%s', %s, '%s', '%s', %s, %s, '%s', '%s')""" % 287 | (department_row.id or 'NULL', user.get('name'), 288 | user.get('workPlace') or "NULL", user.get('userid'), user.get('mobile'), 289 | user.get('email') or 'NULL', user.get('position') or "NULL", 290 | parnter_row and parnter_row.user_ids and parnter_row.user_ids[0].id or 'NULL', 291 | user.get('jobnumber'), user.get('openId'), user.get('isHide'), user.get('active'), 292 | user.get('unionid'), user.get('dingId') 293 | )) 294 | return True 295 | 296 | def get_dingdinguser(self): 297 | department_rows = self.env['ding.department'].search([]) 298 | 299 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 300 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 301 | for department_row in department_rows: 302 | user_dicts = ding_obj.get_depatment_user_list(department_row.department_id) 303 | for user in user_dicts: 304 | if not self.env['ding.user'].search([('ding_id', '=', user.get('userid'))]): 305 | self.get_ding_user(user) 306 | 307 | def get_ding_common_message(self, agent_id=False): 308 | dingding_row = self.env.ref('dingding.ding_ding_xml') 309 | return (dingding_row.corpid, 310 | dingding_row.corpsecret, 311 | agent_id, 312 | { 313 | 'access_token': dingding_row.token, 314 | 'expired_in': dingding_row.expired_in 315 | }) 316 | 317 | 318 | class ding_agent(models.Model): 319 | _name = 'ding.agent' 320 | _description = u'钉钉客户端应用配置' 321 | 322 | name = fields.Char(u'agent名字') 323 | agent_id = fields.Char(u'agent_id', required=True) 324 | ding_id = fields.Many2one('ding.ding', u'钉钉主记录') 325 | 326 | 327 | class AppAgent(models.Model): 328 | _name = 'app.agent' 329 | _description = u'钉钉客户端应用配置' 330 | 331 | name = fields.Char(u'app agent名字') 332 | agent_id = fields.Char(u'app_id', required=True) 333 | app_secret = fields.Char(u'appSecret', required=True) 334 | ding_id = fields.Many2one('ding.ding', u'钉钉主记录') 335 | expired_in = fields.Char(u'过期时间', readonly=True) 336 | token = fields.Char(u'token', readonly=True) 337 | 338 | 339 | class EAgent(models.Model): 340 | _name = 'e.agent' 341 | _description = u'钉钉客户端应用配置' 342 | 343 | name = fields.Char(u'e agent名字') 344 | agent_id = fields.Char(u'AgentId', required=True) 345 | app_secret = fields.Char(u'appSecret', required=True) 346 | app_key = fields.Char(u'AppKey', required=True) 347 | ding_id = fields.Many2one('ding.ding', u'钉钉主记录') 348 | expired_in = fields.Char(u'过期时间', readonly=True) 349 | token = fields.Char(u'token', readonly=True) 350 | 351 | 352 | class ding_department(models.Model): 353 | _name = 'ding.department' 354 | _description = u'钉钉用户部门同步' 355 | _rec_name = 'name' 356 | name = fields.Char(u'部门名字', required=True) 357 | department_id = fields.Char(u'部门id', readonly=True) 358 | parent_id = fields.Many2one('ding.department', u'父部门ID') 359 | ding_id = fields.Char(related='parent_id.department_id', readonly=True, string='Parent name') 360 | parent_order = fields.Char(string='Parent name') 361 | 362 | @api.model 363 | def create(self, vals): 364 | return_vals = super(ding_department, self).create(vals) 365 | if not self.env.context.get('from_dingding'): 366 | return_vals.create_department() 367 | return return_vals 368 | 369 | def create_department(self): 370 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 371 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 372 | for deparment in self: 373 | ding_id = ding_obj.create_dept(deparment.name, 374 | deparment.ding_id, deparment.parent_order) 375 | deparment.department_id = ding_id 376 | return True 377 | 378 | @api.multi 379 | def write(self, vals): 380 | return_vals = super(ding_department, self).write(vals) 381 | if not self.env.context.get('from_dingding'): 382 | self.update_department(vals) 383 | return return_vals 384 | 385 | def update_department(self, vals): 386 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 387 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 388 | for deparment in self: 389 | vals.update({'id': deparment.name}) 390 | ding_obj.update_dept(vals) 391 | return True 392 | 393 | @api.multi 394 | def unlink(self): 395 | return_vals = super(ding_department, self).unlink() 396 | if not self.env.context.get('from_dingding'): 397 | self.delete_department() 398 | return return_vals 399 | 400 | def delete_department(self): 401 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 402 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 403 | for deparment in self: 404 | ding_obj.delete_dept(deparment.name) 405 | return True 406 | 407 | 408 | class ding_user(models.Model): 409 | _name = 'ding.user' 410 | _description = u'钉钉用户 和客户信息的同步' 411 | 412 | ding_user_id = fields.Many2one('res.users', string=u'系统用户') 413 | name = fields.Char(u'钉钉用户', required=True) 414 | sex = fields.Char(u'性别') 415 | mobile_num = fields.Char(u'钉钉手机号', required=True) 416 | email = fields.Char(u'邮箱') 417 | ding_id = fields.Char(u'钉钉用户唯一标识') 418 | ding_code = fields.Char(u'工号') 419 | department_id = fields.Many2one('ding.department', u'部门id') 420 | department_name = fields.Char(related='department_id.name', string=u'部门id') 421 | work_place = fields.Char(u'办公地点') 422 | tel = fields.Char(u'电话') 423 | position = fields.Char(u'职位') 424 | openid = fields.Char(u'openId') 425 | ishide = fields.Boolean(u'isHide') 426 | active = fields.Boolean(u'active') 427 | unionid = fields.Char(u'unionid') 428 | jobnumber = fields.Char(u'jobnumber') 429 | dingding_id = fields.Char(u'dingding_id') 430 | open_id = fields.Char(u'open_id') 431 | 432 | def create_ding_user(self): 433 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 434 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 435 | for user in self: 436 | user_vals = dict(name=user.name, department=[user.department_id.department_id] or [1], 437 | mobile=user.mobile_num, tel=user.tel, email=user.email, 438 | workPlace=user.work_place) 439 | user_id = ding_obj.create_user(user_vals) 440 | user.ding_id = user_id 441 | user.ding_user_id.oauth_access_token = user_id 442 | return True 443 | 444 | def delete_user(self): 445 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 446 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 447 | for user in self: 448 | ding_obj.delete_user(user.ding_id) 449 | return True 450 | 451 | @api.multi 452 | def unlink(self): 453 | return_vals = super(ding_user, self).unlink() 454 | if not self.env.context.get('from_dingding'): 455 | self.delete_user() 456 | return return_vals 457 | 458 | @api.model 459 | def create(self, vals): 460 | return_vals = super(ding_user, self).create(vals) 461 | if not self.env.context.get('from_dingding'): 462 | return_vals.create_ding_user() 463 | return return_vals 464 | 465 | @api.multi 466 | def write(self, vals): 467 | return_vals = super(ding_user, self).write(vals) 468 | if not self.env.context.get('from_dingding'): 469 | self.update_user(vals) 470 | return return_vals 471 | 472 | def update_user(self, vals): 473 | """更新用户信息""" 474 | change_keys = {'name': 'name', 'department_id': 'department_id', 475 | 'mobile_num': 'mobile', 'tel': 'tel', 476 | 'work_place': 'workPlace', 'email': 'email'} 477 | vals = {change_keys.get(key): vals.get(key) for key in change_keys.keys() 478 | if key in vals} 479 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message() 480 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict) 481 | for user in self: 482 | vals.update({'userid': user.ding_id, 'name': user.name}) 483 | ding_obj.update_user(vals) 484 | return True 485 | 486 | def send_message(self, message, user_id, agent_id): 487 | """发送消息必须指定agent_id""" 488 | corpid, corpsecret, agent_id, token_dict = self.env['ding.ding'].get_ding_common_message(agent_id) 489 | ding_obj = Dingtalk(corpid=corpid, corpsecret=corpsecret, token=token_dict, agent_id=agent_id) 490 | dinguser_row = self.search([('ding_user_id', '=', user_id)]) 491 | ding_obj.send_text_message(message, dinguser_row.ding_id, '') 492 | return True 493 | 494 | def send_message_test(self): 495 | """发送测试信息""" 496 | for ding_user in self: 497 | for agent in self.env['ding.agent'].search([]): # 传入不同的agent_id就会有不同的应用向你发送消息 498 | self.send_message(u'你好呀!%s' % str(random.random()), ding_user.ding_user_id.id, agent.agent_id) 499 | 500 | 501 | class ResPartner(models.Model): 502 | _inherit = 'res.partner' 503 | department_id = fields.Many2one('ding.department', string='钉钉部門') 504 | 505 | @api.multi 506 | def write(self, vals): 507 | change_keys = {'name': 'name', 'department_id': 'department_id', 508 | 'mobile': 'mobile_num', 'phone': 'tel', 509 | 'street': 'work_place', 'email': 'email'} 510 | return_vals = super(ResPartner, self).write(vals) 511 | for partner in self: 512 | user_row = self.env['ding.user'].search([('ding_user_id.partner_id', '=', partner.id)]) 513 | ding_fields_keys = list(set(change_keys.keys()).intersection(set(vals.keys()))) 514 | if user_row and ding_fields_keys: 515 | user_row[0].write({change_keys.get(key): vals.get(key) for key in ding_fields_keys}) 516 | return return_vals 517 | 518 | 519 | class ResUsers(models.Model): 520 | _inherit = 'res.users' 521 | have_dingding_account = fields.Boolean(u'钉钉账户') 522 | 523 | @api.model 524 | def check_credentials(self, password): 525 | try: 526 | return super(ResUsers, self).check_credentials(password) 527 | except Exception: 528 | res = self.search([('id', '=', self.env.uid), ('oauth_access_token', '=', password)]) 529 | if not res: 530 | raise 531 | 532 | @api.onchange('have_dingding_account') 533 | def onchange_have_dingding_account(self): 534 | if self.have_dingding_account: 535 | waring = {} 536 | if self.partner_id: 537 | if not (self.partner_id.name and self.partner_id.mobile and self.partner_id.department_id): 538 | self.have_dingding_account = False 539 | waring = {'title': u'错误', 'message': u"请先设置业务伙伴的手机号 钉钉部门!"} 540 | else: 541 | self.have_dingding_account = False 542 | waring = {'title': u'错误', 'message': u"请先设置用户对应的业务伙伴!"} 543 | return {'warning': waring} 544 | 545 | def create_dingding_account(self, user_row, have_dingding_account): 546 | if have_dingding_account != 'default_vals' and have_dingding_account: 547 | self.env['ding.user'].create({ 548 | 'ding_user_id': user_row.id, 549 | 'name': user_row.partner_id.name, 550 | 'department_id': user_row.partner_id.department_id.id, 551 | 'mobile_num': user_row.partner_id.mobile, 552 | 'tel': user_row.partner_id.phone, 553 | 'workPlace': user_row.partner_id.street, 554 | 'email': user_row.partner_id.email, 555 | }) 556 | else: 557 | ding_user = self.env['ding.user'].search([('ding_user_id', '=', user_row.id)]) 558 | ding_user and ding_user.unlink() 559 | return True 560 | 561 | @api.multi 562 | def create(self, vals): 563 | user_row = super(ResUsers, self).create(vals) 564 | self.create_dingding_account(user_row, vals.get('have_dingding_account', 'default_vals')) 565 | return user_row 566 | 567 | @api.multi 568 | def write(self, vals): 569 | user_rows = super(ResUsers, self).write(vals) 570 | for user_row in self: 571 | self.create_dingding_account(user_row, 572 | vals.get('have_dingding_account', 573 | 'default_vals')) 574 | return user_rows 575 | 576 | 577 | class ExamineApprove(models.Model): 578 | _name = 'examine.approve' 579 | 580 | agent_id = fields.Many2one('ding.agent', u'企业微应用标识') 581 | process_code = fields.Char(u'审批流的唯一码') 582 | approvers = fields.Many2many('ding.user', 'user_prove_ref', 'user_id', 'appeove_id', string='审批人列表') 583 | cc_list = fields.Many2many('ding.user', 'user_prove_ref', 'user_id', 'appeove_id', string='抄送人列表') 584 | cc_position = fields.Selection([('START', u'审批开始'), ('FINISH', u'审批结束'), ('START_FINISH', u'开始和结束')], 585 | string='抄送时间') 586 | 587 | -------------------------------------------------------------------------------- /dingding/dingtalk_crypto/__init__.py: -------------------------------------------------------------------------------- 1 | import crypto -------------------------------------------------------------------------------- /dingding/dingtalk_crypto/crypto.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import base64 4 | import hashlib 5 | import struct 6 | import string 7 | import random 8 | from Crypto import Random 9 | from Crypto.Cipher import AES 10 | from .pkcs7 import PKCS7 11 | import hmac 12 | 13 | 14 | class DingTalkCrypto(object): 15 | def __init__(self, encode_aes_key, token, key): 16 | """ 17 | 钉钉加密、解密工具 18 | :param encode_aes_key: 数据加密密钥。用于回调数据的加密,长度固定为43个字符,从a-z, A-Z, 0-9共62个字符中选取 19 | :param token: 用于验证签名的 token 20 | :param key: key对于ISV开发来说,填写对应的suite_key,对于普通企业开发,填写企业的corp_id 21 | """ 22 | self._encode_aes_key = encode_aes_key 23 | self._token = token 24 | self._key = key 25 | self._cipher = AES.new(self.aes_key, AES.MODE_CBC, self.iv_vector) 26 | self._pkcs7 = PKCS7(k=32) 27 | self._random = Random.new() 28 | 29 | def decrypt(self, encrypt_text): 30 | """ 31 | 解密钉钉加密数据 32 | :param encrypt_text: encoded text 33 | :return: rand_str, length, msg, corp_id 34 | """ 35 | aes_msg = base64.decodestring(encrypt_text) 36 | pkcs7_text = self._cipher.decrypt(aes_msg) 37 | text = self._pkcs7.decode(pkcs7_text) 38 | rand_str = text[:16] # 16字节随机字符串 39 | length, = struct.unpack('!i', text[16:20]) # 4字节数据长度 40 | msg_end_pos = 20 + length 41 | msg = text[20:msg_end_pos] 42 | key = text[msg_end_pos:] 43 | return rand_str, length, msg, key 44 | 45 | def encrypt(self, text): 46 | """ 47 | 将给定的本文采用钉钉的加密方式加密 48 | :param text: text 49 | :return: encrypt text 50 | """ 51 | rand_str = self.get_random_str() 52 | length = self._length(text) 53 | pkcs7 = PKCS7Encoder() 54 | full_text = pkcs7.encode(rand_str + length + text + self._key) 55 | aes_text = self._cipher.encrypt(full_text) 56 | return base64.encodestring(aes_text) 57 | 58 | @staticmethod 59 | def _length(text): 60 | """ 61 | 获取4字节的消息长度 62 | :param text: text 63 | :return: four bytes binary ascii length of text 64 | """ 65 | l = len(text) 66 | return struct.pack('!i', l) 67 | 68 | def check_signature(self, encrypt_text, timestamp, nonce, signature): 69 | """ 70 | 验证传输的信息的签名是否正确 71 | :param encrypt_text: str 72 | :param timestamp: str 73 | :param nonce: str 74 | :param signature: 签名 75 | :return: boolean 76 | """ 77 | return self._make_signature(encrypt_text, timestamp, nonce, self._token) == signature 78 | 79 | def get_random_str(self): 80 | """ 随机生成16位字符串 81 | @return: 16位字符串 82 | """ 83 | rule = string.letters + string.digits 84 | str = random.sample(rule, 16) 85 | return "".join(str) 86 | 87 | def sign(self, encrypt_text, timestamp, nonce): 88 | """ 89 | 给加密的信息生成签名 90 | :param encrypt_text: str 91 | :return: signature, timestamp, nonce 92 | """ 93 | token = self._token 94 | signature = self._make_signature(encrypt_text, timestamp, nonce, token) 95 | return signature, timestamp, nonce 96 | 97 | def _make_signature(self, encrypt_text, timestamp, nonce, token): 98 | """ 99 | 生成签名 100 | :param encrypt_text: str 101 | :param timestamp: str 102 | :param nonce: str 103 | :param token: str 104 | :return: str 105 | """ 106 | obj = hashlib.sha1(''.join(sorted([token, timestamp, nonce, encrypt_text]))) 107 | return obj.hexdigest() 108 | 109 | @property 110 | def aes_key(self): 111 | return base64.decodestring(self._encode_aes_key + '=') 112 | 113 | @property 114 | def iv_vector(self): 115 | return self.aes_key[:16] 116 | 117 | class PKCS7Encoder(): 118 | """提供基于PKCS7算法的加解密接口""" 119 | 120 | block_size = 32 121 | 122 | def encode(self, text): 123 | """ 对需要加密的明文进行填充补位 124 | @param text: 需要进行填充补位操作的明文 125 | @return: 补齐明文字符串 126 | """ 127 | text_length = len(text) 128 | # 计算需要填充的位数 129 | amount_to_pad = self.block_size - (text_length % self.block_size) 130 | if amount_to_pad == 0: 131 | amount_to_pad = self.block_size 132 | # 获得补位所用的字符 133 | pad = chr(amount_to_pad) 134 | return text + pad * amount_to_pad 135 | 136 | def decode(self, decrypted): 137 | """删除解密后明文的补位字符 138 | @param decrypted: 解密后的明文 139 | @return: 删除补位字符后的明文 140 | """ 141 | pad = ord(decrypted[-1]) 142 | if pad < 1 or pad > 32: 143 | pad = 0 144 | return decrypted[:-pad] -------------------------------------------------------------------------------- /dingding/dingtalk_crypto/pkcs7.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import binascii 4 | import StringIO 5 | 6 | 7 | class PKCS7(object): 8 | """ 9 | RFC 2315: PKCS#7 page 21 10 | 有些加密算法需要加密的数据的长度必需是8的倍数 11 | """ 12 | def __init__(self, k=16): 13 | self.k = k 14 | 15 | def decode(self, text): 16 | """ 17 | 删除 PKCS#7 方式填充的字符串 18 | :param text: str 19 | :return: str 20 | """ 21 | n1 = len(text) 22 | val = int(binascii.hexlify(text[-1]), 16) 23 | if val > self.k: 24 | raise ValueError("Input is not padded or padding is corrupt") 25 | l = n1 - val 26 | return text[:l] 27 | 28 | def encode(self, text): 29 | """ 30 | 安装 PKCS#7 标准填充字符串 31 | :param text: str 32 | :return: str 33 | """ 34 | l = len(text) 35 | output = StringIO.StringIO() 36 | val = self.k - (l % self.k) 37 | for _ in xrange(val): 38 | output.write('%02x' % val) 39 | return text + binascii.unhexlify(output.getvalue()) -------------------------------------------------------------------------------- /dingding/dingtalk_crypto/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import time 4 | import datetime 5 | import StringIO 6 | import random 7 | import string 8 | import contextlib 9 | 10 | alpha = string.letters + string.digits 11 | 12 | 13 | def get_timestamp(): 14 | """ 15 | 生成现在的Epoch时间 16 | :return: int 17 | """ 18 | now = datetime.datetime.now() 19 | return int(time.mktime(now.timetuple())) 20 | 21 | 22 | def random_alpha(length=8): 23 | """ 24 | 随机生成指定长度的 Alpha 字符串 25 | :param length: int 26 | :return: str 27 | """ 28 | with contextlib.closing(StringIO.StringIO()) as buf: 29 | for _ in xrange(length): 30 | buf.write(random.choice(alpha)) 31 | return buf.read() -------------------------------------------------------------------------------- /dingding/security/ir.model.access.csv: -------------------------------------------------------------------------------- 1 | id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink 2 | all_model_app_agent,all_model_app_agent,model_app_agent,,1,0,0,0 3 | 4 | 5 | -------------------------------------------------------------------------------- /dingding/static/src/css/weui3.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gilbert-yuan/odoo_dingding/9711a27b5e7308093a90507617e90e0b6a5d0c8f/dingding/static/src/css/weui3.css -------------------------------------------------------------------------------- /dingding/static/src/js/dingding.js: -------------------------------------------------------------------------------- 1 | odoo.define('dingding.dingdinglogin', function (require) { 2 | "use strict"; 3 | 4 | var core = require('web.core'); 5 | var Widget = require('web.Widget'); 6 | var rpc = require('web.rpc'); 7 | var session = require('web.session'); 8 | var framework = require('web.framework'); 9 | var ajax = require('web.ajax'); 10 | var QWeb = core.qweb; 11 | var _t = core._t; 12 | 13 | }); 14 | -------------------------------------------------------------------------------- /dingding/views/ding_model.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ding.ding.tree 6 | ding.ding 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ding.ding.form 18 | ding.ding 19 | 20 |
21 |
22 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 |
85 |
86 |
87 | 88 | 89 | 钉钉 90 | ding.ding 91 | ir.actions.act_window 92 | form 93 | tree,form 94 | 95 | 96 | 98 | 99 | 100 | ding.ding.tree 101 | ding.user 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 |