├── .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 |
4 |
5 |
6 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 1515646660063
19 |
20 |
21 | 1515646660063
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
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 |
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 |
113 |
114 |
115 |
116 |
117 |
118 | 钉钉
119 | ding.user
120 | ir.actions.act_window
121 | form
122 | tree
123 |
124 |
125 |
127 |
128 |
129 | ding.ding.tree
130 | ding.department
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | 钉钉部门
143 | ding.department
144 | ir.actions.act_window
145 | form
146 | tree
147 |
148 |
149 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 | odootest
196 | ding95b28951cc12e7d835c2f4657eb6378f
197 | mYZM01r9Zj3lMj2x5KdBMcA0v842pP6_GIw_FNNolc1zCujCUzA4eUyuLYflp9
198 |
199 |
200 | odootest
201 | 109277466
202 |
203 |
204 |
205 | 免登录app
206 | dingoaosarn9kswmozjldq
207 | 5KCEF90rE9IhMTFH4UWaa6BBj0Ig146LsBW_qFXdef1gP80FdGH3GiCpS0JW-b1S
208 |
209 |
210 |
211 |
212 |
213 |
--------------------------------------------------------------------------------
/dingding/views/res_users.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | company.form
6 | res.users
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | res.partner.form
20 | res.partner
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/dingding/钉钉使用手册.docx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/gilbert-yuan/odoo_dingding/9711a27b5e7308093a90507617e90e0b6a5d0c8f/dingding/钉钉使用手册.docx
--------------------------------------------------------------------------------