Accept Merge Request #1333: (feature/销售和排程添加消息推送 -> develop)
Merge Request: 销售和排程添加消息推送 Created By: @杨金灵 Reviewed By: @马广威 Approved By: @马广威 Accepted By: @杨金灵 URL: https://jikimo-hn.coding.net/p/jikimo_sfs/d/jikimo_sf/git/merge/1333
This commit is contained in:
1
jikimo_sale_order_message_notify/__init__.py
Normal file
1
jikimo_sale_order_message_notify/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from . import models
|
||||
26
jikimo_sale_order_message_notify/__manifest__.py
Normal file
26
jikimo_sale_order_message_notify/__manifest__.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
{
|
||||
'name': '机企猫智能工厂 消息提醒',
|
||||
'version': '1.0',
|
||||
'summary': '智能工厂消息提醒模块',
|
||||
'sequence': 1,
|
||||
'description': """
|
||||
|
||||
""",
|
||||
'category': 'sf',
|
||||
'website': 'https://www.sf.jikimo.com',
|
||||
'depends': ['jikimo_message_notify'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'data/bussiness_node.xml',
|
||||
# 'views/sf_message_template_view.xml',
|
||||
],
|
||||
'test': [
|
||||
],
|
||||
'license': 'LGPL-3',
|
||||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': False,
|
||||
}
|
||||
9
jikimo_sale_order_message_notify/data/bussiness_node.xml
Normal file
9
jikimo_sale_order_message_notify/data/bussiness_node.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="bussiness_1" model="jikimo.message.bussiness.node">
|
||||
<field name="name">订单确认</field>
|
||||
<field name="model">sale.order</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
2
jikimo_sale_order_message_notify/models/__init__.py
Normal file
2
jikimo_sale_order_message_notify/models/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
||||
from . import jikimo_message_template
|
||||
from . import sale_order
|
||||
@@ -0,0 +1,10 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
class JikimoMessageTemplate(models.Model):
|
||||
_inherit = "jikimo.message.template"
|
||||
|
||||
def _get_message_model(self):
|
||||
res = super(JikimoMessageTemplate, self)._get_message_model()
|
||||
res.append("sale.order")
|
||||
return res
|
||||
|
||||
12
jikimo_sale_order_message_notify/models/sale_order.py
Normal file
12
jikimo_sale_order_message_notify/models/sale_order.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from odoo import models, fields, api
|
||||
|
||||
|
||||
class SaleOrder(models.Model):
|
||||
_name = "sale.order"
|
||||
_description = "销售订单"
|
||||
_inherit = ["sale.order", "jikimo.message.dispatch"]
|
||||
|
||||
def create(self, vals_list):
|
||||
res = super(SaleOrder, self).create(vals_list)
|
||||
res.add_queue('订单确认')
|
||||
return res
|
||||
@@ -0,0 +1,5 @@
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,6 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -0,0 +1,76 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- © <2016> <top hy>
|
||||
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). -->
|
||||
|
||||
<odoo>
|
||||
<data>
|
||||
|
||||
<record id="sf_message_template_view_form" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.view.form</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="消息模板">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<label for="name"/>
|
||||
<h1>
|
||||
<field name="name" class="w-100" required="1"/>
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<!-- <field name="type"/>-->
|
||||
<field name="notify_model_id"/>
|
||||
<field name="content" widget="html" class="oe-bordered-editor"
|
||||
options="{'style-inline': true, 'codeview': true, 'dynamic_placeholder': true}"/>
|
||||
<field name="description"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="notification_department_id"/>
|
||||
<field name="notification_employee_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sf_message_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.view.tree</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="消息模板">
|
||||
<field name="name"/>
|
||||
<!-- <field name="type"/>-->
|
||||
<field name="content"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="notification_department_id"/>
|
||||
<field name="notification_employee_ids" widget="many2many_tags"/>
|
||||
<field name="description"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="sf_message_template_search_view" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.search.view</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" string="模糊搜索"
|
||||
filter_domain="['|','|',('name','like',self),('description','like',self)]"/>
|
||||
<field name="name"/>
|
||||
<filter name="filter_active" string="已归档" domain="[('active','=',False)]"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!--定义单证类型视图动作-->
|
||||
<record id="sf_message_template_action" model="ir.actions.act_window">
|
||||
<field name="name">消息模板</field>
|
||||
<field name="res_model">message.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="sf_message_template_view_tree"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="msg_set_menu" name="消息设置" parent="base.menu_administration" sequence="1"/>
|
||||
<menuitem id="sf_message_template_send_menu" name="消息模板" parent="msg_set_menu"
|
||||
action="sf_message_template_action" sequence="1"/>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,2 +1,3 @@
|
||||
pystrich
|
||||
cpca
|
||||
pycryptodome==3.20
|
||||
|
||||
@@ -11,10 +11,9 @@
|
||||
""",
|
||||
'category': 'sf',
|
||||
'website': 'https://www.sf.jikimo.com',
|
||||
'depends': ['base', 'sf_plan', 'sf_sale'],
|
||||
'depends': ['sale', 'purchase', 'sf_plan', 'jikimo_message_notify'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/sf_message_template_view.xml',
|
||||
'data/bussiness_node.xml'
|
||||
],
|
||||
'test': [
|
||||
],
|
||||
|
||||
14
sf_message/data/bussiness_node.xml
Normal file
14
sf_message/data/bussiness_node.xml
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" ?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<record id="bussiness_pending_order" model="jikimo.message.bussiness.node">
|
||||
<field name="name">待接单</field>
|
||||
<field name="model">sale.order</field>
|
||||
</record>
|
||||
|
||||
<record id="bussiness_to_be_confirm" model="jikimo.message.bussiness.node">
|
||||
<field name="name">确认接单</field>
|
||||
<field name="model">sale.order</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
|
||||
|
||||
class SFMessageCamProgram(models.Model):
|
||||
_name = 'sf.cam.work.order.program.knife.plan'
|
||||
_inherit = ['sf.cam.work.order.program.knife.plan', 'sf.message.template']
|
||||
_inherit = ['sf.cam.work.order.program.knife.plan', 'jikimo.message.dispatch']
|
||||
|
||||
@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
|
||||
|
||||
class SFMessagefunctionalToolAssembly(models.Model):
|
||||
_name = 'sf.functional.tool.assembly'
|
||||
_inherit = ['sf.functional.tool.assembly', 'sf.message.template']
|
||||
_inherit = ['sf.functional.tool.assembly', 'jikimo.message.dispatch']
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessagePlan(models.Model):
|
||||
_name = 'sf.production.plan'
|
||||
_inherit = ['sf.production.plan', 'sf.message.template']
|
||||
_inherit = ['sf.production.plan', 'jikimo.message.dispatch']
|
||||
|
||||
# def create(self, vals_list):
|
||||
# res = super(SFMessagePlan, self).create(vals_list)
|
||||
# if res:
|
||||
# try:
|
||||
# res.add_queue('待排程')
|
||||
# except Exception as e:
|
||||
# logging.info('add_queue error:%s' % e)
|
||||
# return res
|
||||
#
|
||||
# def _get_message(self):
|
||||
# res = super(SFMessagePlan, self)._get_message()
|
||||
# if res:
|
||||
# try:
|
||||
# res.add_queue('待排程')
|
||||
# except Exception as e:
|
||||
# logging.info('_get_message error:%s' % e)
|
||||
# return res
|
||||
|
||||
@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
|
||||
|
||||
class SFMessagePurchase(models.Model):
|
||||
_name = 'purchase.order'
|
||||
_inherit = ['purchase.order', 'sf.message.template']
|
||||
_inherit = ['purchase.order', 'jikimo.message.dispatch']
|
||||
|
||||
@@ -1,11 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
|
||||
|
||||
class SFMessageSale(models.Model):
|
||||
_name = 'sale.order'
|
||||
_inherit = ['sale.order', 'sf.message.template']
|
||||
_inherit = ['sale.order', 'jikimo.message.dispatch']
|
||||
|
||||
# def create(self):
|
||||
# res = super(SFMessageSale, self).create()
|
||||
# if res is True:
|
||||
def create(self, vals_list):
|
||||
res = super(SFMessageSale, self).create(vals_list)
|
||||
if res:
|
||||
try:
|
||||
res.add_queue('待接单')
|
||||
except Exception as e:
|
||||
logging.info('add_queue error:%s' % e)
|
||||
return res
|
||||
|
||||
# 确认接单
|
||||
def action_confirm(self):
|
||||
res = super(SFMessageSale, self).action_confirm()
|
||||
if res is True:
|
||||
try:
|
||||
self.add_queue('确认接单')
|
||||
except Exception as e:
|
||||
logging.info('add_queue error:%s' % e)
|
||||
return res
|
||||
|
||||
# 继承并重写jikimo.message.dispatch的_get_message()
|
||||
def _get_message(self, message_queue_ids):
|
||||
res = super(SFMessageSale, self)._get_message(message_queue_ids)
|
||||
if message_queue_ids.message_template_id.bussiness_node_id.name == '确认接单':
|
||||
# sale_order = self.env['sale.order'].search([('id', '=', message_queue_ids.model.res_id)])
|
||||
sale_order_line = self.env['sale.order.line'].search([('order_id', '=', int(message_queue_ids.res_id))])
|
||||
if len(sale_order_line) == 1:
|
||||
product = sale_order_line[0].product_id.name
|
||||
elif len(sale_order_line) > 1:
|
||||
product = '%s...' % sale_order_line[0].product_id.name
|
||||
res[0] = res[0].replace('{{product_id}}', product)
|
||||
return res
|
||||
|
||||
@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
|
||||
|
||||
class SFMessageStockPicking(models.Model):
|
||||
_name = 'stock.picking'
|
||||
_inherit = ['stock.picking', 'sf.message.template']
|
||||
_inherit = ['stock.picking', 'jikimo.message.dispatch']
|
||||
|
||||
@@ -4,31 +4,9 @@ from abc import ABC, abstractmethod
|
||||
|
||||
|
||||
class SfMessageTemplate(models.Model):
|
||||
_name = "sf.message.template"
|
||||
_description = u'消息模板'
|
||||
_inherit = "jikimo.message.template"
|
||||
|
||||
name = fields.Char(string=u"名称", required=True)
|
||||
description = fields.Char(string=u"描述")
|
||||
content = fields.Html(string=u"内容", render_engine='qweb', translate=True, prefetch=True, sanitize=False)
|
||||
msgtype = fields.Selection(
|
||||
[('text', u'文字'), ('markdown', u'Markdown')], u'消息类型',
|
||||
required=True, default='markdown')
|
||||
notification_department_id = fields.Many2one('hr.department', u'通知部门', required=True)
|
||||
notification_employee_ids = fields.Many2many('hr.employee', string=u'员工',
|
||||
domain="[('department_id', '=',notification_department_id)]",
|
||||
required=True)
|
||||
is_send_time = fields.Boolean(string=u"定时发送", default=False)
|
||||
send_time_1 = fields.Integer('发送时间点1')
|
||||
send_time_2 = fields.Integer('发送时间点2')
|
||||
active = fields.Boolean(string=u"是否有效", default=True)
|
||||
|
||||
@api.onchange('notification_department_id')
|
||||
def _clear_employee_ids(self):
|
||||
if self.notification_department_id:
|
||||
self.notification_employee_ids = False
|
||||
|
||||
@abstractmethod
|
||||
def dispatch(self, args):
|
||||
"""
|
||||
强迫继承该类必走该抽象方法'
|
||||
"""
|
||||
def _get_message_model(self):
|
||||
res = super(SfMessageTemplate, self)._get_message_model()
|
||||
res.append("sale.order")
|
||||
return res
|
||||
|
||||
@@ -3,4 +3,4 @@ from odoo import models, fields, api, _
|
||||
|
||||
class SFMessageWork(models.Model):
|
||||
_name = 'mrp.workorder'
|
||||
_inherit = ['mrp.workorder', 'sf.message.template']
|
||||
_inherit = ['mrp.workorder', 'jikimo.message.dispatch']
|
||||
|
||||
@@ -1,9 +1,23 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_sf_message_template_group_sale_salemanager,sf_message_template,model_sf_message_template,sf_base.group_sale_salemanager,1,1,1,0
|
||||
access_sf_message_template_group_purchase,sf_message_template,model_sf_message_template,sf_base.group_purchase,1,1,1,0
|
||||
access_sf_message_template_group_sf_stock_user,sf_message_template,model_sf_message_template,sf_base.group_sf_stock_user,1,1,1,0
|
||||
access_sf_message_template_group_sf_order_user,sf_message_template,model_sf_message_template,sf_base.group_sf_order_user,1,1,1,0
|
||||
access_sf_message_template_group_sf_tool_user,sf_message_template,model_sf_message_template,sf_base.group_sf_tool_user,1,1,1,0
|
||||
|
||||
access_jikimo_message_template_group_sale_salemanager,jikimo_message_template,model_jikimo_message_template,sf_base.group_sale_salemanager,1,1,1,0
|
||||
access_jikimo_message_template_group_purchase,jikimo_message_template,model_jikimo_message_template,sf_base.group_purchase,1,1,1,0
|
||||
access_jikimo_message_template_group_sf_stock_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_stock_user,1,1,1,0
|
||||
access_jikimo_message_template_group_sf_order_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_order_user,1,1,1,0
|
||||
access_jikimo_message_template_group_sf_tool_user,jikimo_message_template,model_jikimo_message_template,sf_base.group_sf_tool_user,1,1,1,0
|
||||
|
||||
access_jikimo_message_bussiness_node_group_sale_salemanager,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sale_salemanager,1,1,1,0
|
||||
access_jikimo_message_bussiness_node_group_purchase,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_purchase,1,1,1,0
|
||||
access_jikimo_message_bussiness_node_group_sf_stock_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_stock_user,1,1,1,0
|
||||
access_jikimo_message_bussiness_node_group_sf_order_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_order_user,1,1,1,0
|
||||
access_jikimo_message_bussiness_node_group_sf_tool_user,jikimo_message_bussiness_node,model_jikimo_message_bussiness_node,sf_base.group_sf_tool_user,1,1,1,0
|
||||
|
||||
access_jikimo_message_queue_group_sale_salemanager,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sale_salemanager,1,1,1,0
|
||||
access_jikimo_message_queue_group_purchase,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_purchase,1,1,1,0
|
||||
access_jikimo_message_queue_group_sf_stock_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_stock_user,1,1,1,0
|
||||
access_jikimo_message_queue_group_sf_order_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_order_user,1,1,1,0
|
||||
access_jikimo_message_queue_group_sf_tool_user,jikimo_message_queue,model_jikimo_message_queue,sf_base.group_sf_tool_user,1,1,1,0
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -7,7 +7,7 @@
|
||||
|
||||
<record id="sf_message_template_view_form" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.view.form</field>
|
||||
<field name="model">sf.message.template</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="消息模板">
|
||||
<sheet>
|
||||
@@ -18,14 +18,12 @@
|
||||
</h1>
|
||||
</div>
|
||||
<group>
|
||||
<!-- <field name="type"/>-->
|
||||
<!-- <field name="type"/>-->
|
||||
<field name="notify_model_id"/>
|
||||
<field name="content" widget="html" class="oe-bordered-editor"
|
||||
options="{'style-inline': true, 'codeview': true, 'dynamic_placeholder': true}"/>
|
||||
<field name="description"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="is_send_time"/>
|
||||
<field name="send_time_1" attrs="{'invisible': [('is_send_time', '=', False)]}"/>
|
||||
<field name="send_time_2" attrs="{'invisible': [('is_send_time', '=', False)]}"/>
|
||||
<field name="notification_department_id"/>
|
||||
<field name="notification_employee_ids" widget="many2many_tags"/>
|
||||
</group>
|
||||
@@ -36,16 +34,13 @@
|
||||
|
||||
<record id="sf_message_template_view_tree" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.view.tree</field>
|
||||
<field name="model">sf.message.template</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="消息模板">
|
||||
<field name="name"/>
|
||||
<!-- <field name="type"/>-->
|
||||
<!-- <field name="type"/>-->
|
||||
<field name="content"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="is_send_time"/>
|
||||
<field name="send_time_1" attrs="{'invisible': [('is_send_time', '=', False)]}"/>
|
||||
<field name="send_time_2" attrs="{'invisible': [('is_send_time', '=', False)]}"/>
|
||||
<field name="notification_department_id"/>
|
||||
<field name="notification_employee_ids" widget="many2many_tags"/>
|
||||
<field name="description"/>
|
||||
@@ -55,7 +50,7 @@
|
||||
|
||||
<record id="sf_message_template_search_view" model="ir.ui.view">
|
||||
<field name="name">sf.message.template.search.view</field>
|
||||
<field name="model">sf.message.template</field>
|
||||
<field name="model">message.template</field>
|
||||
<field name="arch" type="xml">
|
||||
<search>
|
||||
<field name="name" string="模糊搜索"
|
||||
@@ -69,7 +64,7 @@
|
||||
<!--定义单证类型视图动作-->
|
||||
<record id="sf_message_template_action" model="ir.actions.act_window">
|
||||
<field name="name">消息模板</field>
|
||||
<field name="res_model">sf.message.template</field>
|
||||
<field name="res_model">message.template</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="view_id" ref="sf_message_template_view_tree"/>
|
||||
</record>
|
||||
|
||||
138
sg_wechat_enterprise/.gitignore
vendored
138
sg_wechat_enterprise/.gitignore
vendored
@@ -1,138 +0,0 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
@@ -1,3 +0,0 @@
|
||||
from . import we_api
|
||||
from . import models
|
||||
from . import controllers
|
||||
@@ -1,52 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
{
|
||||
'name': '企业微信',
|
||||
'version': '0.1',
|
||||
'summary': '企业通讯录\消息处理\企业应用\无缝登录',
|
||||
'sequence': 30,
|
||||
"author": 'SmartGo Studio.,',
|
||||
'description': '''用于企业内部员工的管理,
|
||||
ER企业微信模块
|
||||
=====================================================
|
||||
主要针对odoo使用微信进行管理,包括以下功能:
|
||||
1) 公众号信息管理(企业号下多applicaiton管理)
|
||||
2) 接收消息处理
|
||||
3) 发送消息处理
|
||||
4) 自定义菜单处理
|
||||
|
||||
....
|
||||
本安装包使用了WechatEnterpriseSDK/wechat_sdk.py,在此表示感谢。
|
||||
源代码可以访问github.地址如下:https://github.com/facert/WechatEnterpriseSDK
|
||||
|
||||
''',
|
||||
'category': '基础信息',
|
||||
'website': 'https://www.smartgo.cn',
|
||||
'depends': ['base', 'mail','hr'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'views/we_config_view.xml',
|
||||
'views/we_app_view.xml',
|
||||
'views/we_send_message_view.xml',
|
||||
'views/we_receive_message_view.xml',
|
||||
'views/we_message_process_view.xml',
|
||||
'views/we_templates.xml',
|
||||
# "views/mail_view.xml",
|
||||
"views/res_users_view.xml",
|
||||
'views/menu_view.xml',
|
||||
# 'views/we_menu.xml',
|
||||
"data/data.xml"
|
||||
],
|
||||
'demo': [
|
||||
'demo/we_config_demo.xml',
|
||||
],
|
||||
'qweb': [
|
||||
# "static/src/xml/base.xml",
|
||||
# "static/src/xml/account_payment.xml",
|
||||
# "static/src/xml/account_report_backend.xml",
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
# 'post_init_hook': '_auto_install_l10n',
|
||||
}
|
||||
@@ -1,273 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding:utf-8 -*-
|
||||
|
||||
""" 对企业微信发送给企业后台的消息加解密示例代码.
|
||||
@copyright: Copyright (c) 1998-2014 Tencent Inc.
|
||||
|
||||
"""
|
||||
# ------------------------------------------------------------------------
|
||||
|
||||
import base64
|
||||
import string
|
||||
import random
|
||||
import hashlib
|
||||
import time
|
||||
import struct
|
||||
from Crypto.Cipher import AES
|
||||
import xml.etree.cElementTree as ET
|
||||
import socket
|
||||
from . import ierror
|
||||
|
||||
"""
|
||||
关于Crypto.Cipher模块,ImportError: No module named 'Crypto'解决方案
|
||||
请到官方网站 https://www.dlitz.net/software/pycrypto/ 下载pycrypto。
|
||||
下载后,按照README中的“Installation”小节的提示进行pycrypto安装。
|
||||
"""
|
||||
|
||||
|
||||
class FormatException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def throw_exception(message, exception_class=FormatException):
|
||||
"""my define raise exception function"""
|
||||
raise exception_class(message)
|
||||
|
||||
|
||||
class SHA1:
|
||||
"""计算企业微信的消息签名接口"""
|
||||
|
||||
def getSHA1(self, token, timestamp, nonce, encrypt):
|
||||
"""用SHA1算法生成安全签名
|
||||
@param token: 票据
|
||||
@param timestamp: 时间戳
|
||||
@param encrypt: 密文
|
||||
@param nonce: 随机字符串
|
||||
@return: 安全签名
|
||||
"""
|
||||
try:
|
||||
sortlist = [token, timestamp, nonce, encrypt]
|
||||
sortlist.sort()
|
||||
sha = hashlib.sha1()
|
||||
sort_str = "".join(sortlist)
|
||||
sha.update(sort_str.encode('utf-8'))
|
||||
return ierror.WXBizMsgCrypt_OK, sha.hexdigest()
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None
|
||||
|
||||
|
||||
class XMLParse:
|
||||
"""提供提取消息格式中的密文及生成回复消息格式的接口"""
|
||||
|
||||
# xml消息模板
|
||||
AES_TEXT_RESPONSE_TEMPLATE = """<xml>
|
||||
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt>
|
||||
<MsgSignature><![CDATA[%(msg_signaturet)s]]></MsgSignature>
|
||||
<TimeStamp>%(timestamp)s</TimeStamp>
|
||||
<Nonce><![CDATA[%(nonce)s]]></Nonce>
|
||||
</xml>"""
|
||||
|
||||
def extract(self, xmltext):
|
||||
"""提取出xml数据包中的加密消息
|
||||
@param xmltext: 待提取的xml字符串
|
||||
@return: 提取出的加密消息字符串
|
||||
"""
|
||||
try:
|
||||
xml_tree = ET.fromstring(xmltext)
|
||||
encrypt = xml_tree.find("Encrypt")
|
||||
touser_name = xml_tree.find("ToUserName")
|
||||
return ierror.WXBizMsgCrypt_OK, encrypt.text, touser_name.text
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_ParseXml_Error, None, None
|
||||
|
||||
def generate(self, encrypt, signature, timestamp, nonce):
|
||||
"""生成xml消息
|
||||
@param encrypt: 加密后的消息密文
|
||||
@param signature: 安全签名
|
||||
@param timestamp: 时间戳
|
||||
@param nonce: 随机字符串
|
||||
@return: 生成的xml字符串
|
||||
"""
|
||||
resp_dict = {
|
||||
'msg_encrypt': encrypt,
|
||||
'msg_signaturet': signature,
|
||||
'timestamp': timestamp,
|
||||
'nonce': nonce,
|
||||
}
|
||||
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict
|
||||
return resp_xml
|
||||
|
||||
|
||||
class PKCS7Encoder():
|
||||
"""提供基于PKCS7算法的加解密接口"""
|
||||
|
||||
block_size = 32
|
||||
|
||||
def encode(self, text):
|
||||
""" 对需要加密的明文进行填充补位
|
||||
@param text: 需要进行填充补位操作的明文
|
||||
@return: 补齐明文字符串
|
||||
"""
|
||||
text_length = len(text)
|
||||
# 计算需要填充的位数
|
||||
amount_to_pad = self.block_size - (text_length % self.block_size)
|
||||
if amount_to_pad == 0:
|
||||
amount_to_pad = self.block_size
|
||||
# 获得补位所用的字符
|
||||
pad = chr(amount_to_pad)
|
||||
return text + pad * amount_to_pad
|
||||
|
||||
def decode(self, decrypted):
|
||||
"""删除解密后明文的补位字符
|
||||
@param decrypted: 解密后的明文
|
||||
@return: 删除补位字符后的明文
|
||||
"""
|
||||
pad = ord(decrypted[-1])
|
||||
if pad < 1 or pad > 32:
|
||||
pad = 0
|
||||
return decrypted[:-pad]
|
||||
|
||||
|
||||
class Prpcrypt(object):
|
||||
"""提供接收和推送给企业微信消息的加解密接口"""
|
||||
|
||||
def __init__(self, key):
|
||||
|
||||
# self.key = base64.b64decode(key+"=")
|
||||
self.key = key
|
||||
# 设置加解密模式为AES的CBC模式
|
||||
self.mode = AES.MODE_CBC
|
||||
|
||||
def encrypt(self, text, corpid):
|
||||
"""对明文进行加密
|
||||
@param text: 需要加密的明文
|
||||
@return: 加密得到的字符串
|
||||
"""
|
||||
# 16位随机字符串添加到明文开头
|
||||
text = self.get_random_str() + str(struct.pack("I", socket.htonl(len(text))), encoding='utf8')\
|
||||
+ str(text, encoding='utf8') + corpid
|
||||
# 使用自定义的填充方式对明文进行补位填充
|
||||
pkcs7 = PKCS7Encoder()
|
||||
text = pkcs7.encode(text)
|
||||
# 加密
|
||||
cryptor = AES.new(self.key, self.mode, self.key[:16])
|
||||
try:
|
||||
ciphertext = cryptor.encrypt(text)
|
||||
# 使用BASE64对加密后的字符串进行编码
|
||||
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext)
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_EncryptAES_Error, None
|
||||
|
||||
def decrypt(self, text, corpid):
|
||||
"""对解密后的明文进行补位删除
|
||||
@param text: 密文
|
||||
@return: 删除填充补位后的明文
|
||||
"""
|
||||
try:
|
||||
cryptor = AES.new(self.key, self.mode, self.key[:16])
|
||||
# 使用BASE64对密文进行解码,然后AES-CBC解密
|
||||
plain_text = cryptor.decrypt(base64.b64decode(text))
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_DecryptAES_Error, None
|
||||
try:
|
||||
pad = plain_text[-1]
|
||||
# 去掉补位字符串
|
||||
# pkcs7 = PKCS7Encoder()
|
||||
# plain_text = pkcs7.encode(plain_text)
|
||||
# 去除16位随机字符串
|
||||
content = plain_text[16:-pad]
|
||||
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0])
|
||||
xml_content = content[4: xml_len + 4]
|
||||
from_corpid = content[xml_len + 4:]
|
||||
except Exception as e:
|
||||
return ierror.WXBizMsgCrypt_IllegalBuffer, None
|
||||
if str(from_corpid, encoding="utf8") != corpid and len(ET.fromstring(xml_content).findall("SuiteId")) < 1:
|
||||
return ierror.WXBizMsgCrypt_ValidateCorpid_Error, None
|
||||
return 0, xml_content
|
||||
|
||||
def get_random_str(self):
|
||||
""" 随机生成16位字符串
|
||||
@return: 16位字符串
|
||||
"""
|
||||
rule = string.ascii_letters + string.digits
|
||||
str = random.sample(rule, 16)
|
||||
return "".join(str)
|
||||
|
||||
|
||||
class WXBizMsgCrypt(object):
|
||||
# 构造函数
|
||||
# @param sToken: 企业微信后台,开发者设置的Token
|
||||
# @param sEncodingAESKey: 企业微信后台,开发者设置的EncodingAESKey
|
||||
# @param sCorpId: 企业号的CorpId
|
||||
def __init__(self, sToken, sEncodingAESKey, sCorpId):
|
||||
try:
|
||||
self.key = base64.b64decode(sEncodingAESKey + "=")
|
||||
assert len(self.key) == 32
|
||||
except:
|
||||
throw_exception("[error]: EncodingAESKey unvalid !", FormatException)
|
||||
# return ierror.WXBizMsgCrypt_IllegalAesKey,None
|
||||
self.m_sToken = sToken
|
||||
self.m_sCorpid = sCorpId
|
||||
|
||||
# 验证URL
|
||||
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
|
||||
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
|
||||
# @param sNonce: 随机串,对应URL参数的nonce
|
||||
# @param sEchoStr: 随机串,对应URL参数的echostr
|
||||
# @param sReplyEchoStr: 解密之后的echostr,当return返回0时有效
|
||||
# @return:成功0,失败返回对应的错误码
|
||||
|
||||
def VerifyURL(self, sMsgSignature, sTimeStamp, sNonce, sEchoStr):
|
||||
sha1 = SHA1()
|
||||
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, sEchoStr)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
if not signature == sMsgSignature:
|
||||
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
|
||||
pc = Prpcrypt(self.key)
|
||||
ret, sReplyEchoStr = pc.decrypt(sEchoStr, self.m_sCorpid)
|
||||
return ret, sReplyEchoStr
|
||||
|
||||
def EncryptMsg(self, sReplyMsg, sNonce, timestamp=None):
|
||||
# 将企业回复用户的消息加密打包
|
||||
# @param sReplyMsg: 企业号待回复用户的消息,xml格式的字符串
|
||||
# @param sTimeStamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间
|
||||
# @param sNonce: 随机串,可以自己生成,也可以用URL参数的nonce
|
||||
# sEncryptMsg: 加密后的可以直接回复用户的密文,包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串,
|
||||
# return:成功0,sEncryptMsg,失败返回对应的错误码None
|
||||
pc = Prpcrypt(self.key)
|
||||
ret, encrypt = pc.encrypt(sReplyMsg, self.m_sCorpid)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
if timestamp is None:
|
||||
timestamp = str(int(time.time()))
|
||||
# 生成安全签名
|
||||
sha1 = SHA1()
|
||||
ret, signature = sha1.getSHA1(self.m_sToken, timestamp, sNonce, encrypt)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
xmlParse = XMLParse()
|
||||
return ret, xmlParse.generate(encrypt, signature, timestamp, sNonce)
|
||||
|
||||
def DecryptMsg(self, sPostData, sMsgSignature, sTimeStamp, sNonce):
|
||||
# 检验消息的真实性,并且获取解密后的明文
|
||||
# @param sMsgSignature: 签名串,对应URL参数的msg_signature
|
||||
# @param sTimeStamp: 时间戳,对应URL参数的timestamp
|
||||
# @param sNonce: 随机串,对应URL参数的nonce
|
||||
# @param sPostData: 密文,对应POST请求的数据
|
||||
# xml_content: 解密后的原文,当return返回0时有效
|
||||
# @return: 成功0,失败返回对应的错误码
|
||||
# 验证安全签名
|
||||
xmlParse = XMLParse()
|
||||
ret, encrypt, touser_name = xmlParse.extract(sPostData)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
sha1 = SHA1()
|
||||
ret, signature = sha1.getSHA1(self.m_sToken, sTimeStamp, sNonce, encrypt)
|
||||
if ret != 0:
|
||||
return ret, None
|
||||
if not signature == sMsgSignature:
|
||||
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None
|
||||
pc = Prpcrypt(self.key)
|
||||
ret, xml_content = pc.decrypt(encrypt, self.m_sCorpid)
|
||||
return ret, xml_content
|
||||
@@ -1,3 +0,0 @@
|
||||
|
||||
from . import wechat_enterprise
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
#########################################################################
|
||||
# Author: jonyqin
|
||||
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST
|
||||
# File Name: ierror.py
|
||||
# Description:定义错误码含义
|
||||
#########################################################################
|
||||
WXBizMsgCrypt_OK = 0
|
||||
WXBizMsgCrypt_ValidateSignature_Error = -40001
|
||||
WXBizMsgCrypt_ParseXml_Error = -40002
|
||||
WXBizMsgCrypt_ComputeSignature_Error = -40003
|
||||
WXBizMsgCrypt_IllegalAesKey = -40004
|
||||
WXBizMsgCrypt_ValidateCorpid_Error = -40005
|
||||
WXBizMsgCrypt_EncryptAES_Error = -40006
|
||||
WXBizMsgCrypt_DecryptAES_Error = -40007
|
||||
WXBizMsgCrypt_IllegalBuffer = -40008
|
||||
WXBizMsgCrypt_EncodeBase64_Error = -40009
|
||||
WXBizMsgCrypt_DecodeBase64_Error = -40010
|
||||
WXBizMsgCrypt_GenReturnXml_Error = -40011
|
||||
@@ -1,231 +0,0 @@
|
||||
import time
|
||||
import functools
|
||||
import base64
|
||||
from json import *
|
||||
|
||||
from odoo import http, fields
|
||||
from odoo.http import request
|
||||
from . import WXBizMsgCrypt
|
||||
from werkzeug.exceptions import abort
|
||||
import xml.etree.cElementTree as Et
|
||||
import requests as req
|
||||
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def wechat_login(func):
|
||||
"""
|
||||
用来根据userid取得合作伙伴的id
|
||||
:return:
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kw):
|
||||
# now_time = time.time()
|
||||
# request.session['session_time'] = time.time()
|
||||
# if 'session_time' not in request.session:
|
||||
# request.session['session_time'] = 1
|
||||
# if now_time > request.session['session_time'] + 350:
|
||||
_logger.info(u"没进来之前的kw值为%s" % JSONEncoder().encode(kw))
|
||||
# if not request.session['login'] or not request.session['login'] or request.session[
|
||||
# 'login'] == "public":
|
||||
enterprise, agent_id = request.env['we.config'].sudo().get_odoo_wechat()
|
||||
if enterprise and agent_id:
|
||||
if 'code' in kw and 'state' in kw: # 检查是否取得了code
|
||||
if 'kw' in request.session.keys():
|
||||
_logger.info('code:%s' % kw['code'])
|
||||
account = enterprise.oauth.get_user_info(code=kw['code'])
|
||||
_logger.info('account:%s' % account)
|
||||
if account: # 是否取得了微信企业号通讯录的账号
|
||||
user_detail = enterprise.user.get_detail(account['user_ticket'])
|
||||
_logger.info('user_detail:%s' % user_detail)
|
||||
request.env['we.employee'].we_privacy_update(user_detail)
|
||||
user = request.env['res.users'].sudo().search(
|
||||
[('we_employee_id', '=', account['UserId'])])
|
||||
_logger.info('user:%s' % user)
|
||||
if user: # 是否取得了用户
|
||||
if 'state' in kw:
|
||||
state = base64.b64encode(kw['state'].encode('utf-8')).decode()
|
||||
kw['state'] = state
|
||||
uid = request.session.authenticate(request.session.db, user.login,
|
||||
account['UserId'])
|
||||
kw['user_id'] = uid
|
||||
request.session['session_time'] = time.time()
|
||||
request.session['login'] = user.login
|
||||
|
||||
_logger.info(u"进来之后的kw值为%s" % kw)
|
||||
return func(*args, **kw)
|
||||
else:
|
||||
_logger.warning(u'用户不存在.')
|
||||
return request.render('sg_wechat_enterprise.wechat_warning',
|
||||
{'title': u'警告', 'content': u'该员工的未配置登录用户.'})
|
||||
else:
|
||||
_logger.warning(u'微信企业号验证失败.')
|
||||
return request.render('sg_wechat_enterprise.wechat_warning',
|
||||
{'title': u'警告', 'content': u'微信企业号验证失败.'})
|
||||
else:
|
||||
# 返回时候进入的地方
|
||||
del kw['code']
|
||||
del kw['state']
|
||||
request.session['kw'] = base64.b64encode(JSONEncoder().encode(kw).encode('utf-8')).decode()
|
||||
if len(kw) == 0:
|
||||
base_url = request.httprequest.base_url
|
||||
else:
|
||||
base_url = request.httprequest.base_url + '?'
|
||||
for item, value in kw.items():
|
||||
base_url += item + '=' + value + "&"
|
||||
base_url = base_url[: -1]
|
||||
|
||||
url = enterprise.oauth.authorize_url(base_url,
|
||||
state=base64.b64encode(
|
||||
JSONEncoder().encode(kw).encode('utf-8')).decode(),
|
||||
agent_id=agent_id,
|
||||
scope='snsapi_privateinfo')
|
||||
_logger.warning(u"这是授权的url:" + url)
|
||||
value = {"url": url}
|
||||
return request.render("sg_wechat_enterprise.Transfer", value)
|
||||
else: # 开始微信企业号登录认证
|
||||
request.session['kw'] = base64.b64encode(JSONEncoder().encode(kw).encode('utf-8')).decode()
|
||||
|
||||
if len(kw) == 0:
|
||||
base_url = request.httprequest.base_url
|
||||
else:
|
||||
base_url = request.httprequest.base_url + '?'
|
||||
for item, value in kw.items():
|
||||
base_url += item + '=' + value + "&"
|
||||
base_url = base_url[: -1]
|
||||
url = enterprise.oauth.authorize_url(base_url,
|
||||
state=base64.b64encode(
|
||||
JSONEncoder().encode(kw).encode('utf-8')).decode(),
|
||||
agent_id=agent_id,
|
||||
scope='snsapi_privateinfo'
|
||||
)
|
||||
_logger.warning(u"这是授权的url:" + url)
|
||||
value = {"url": url}
|
||||
return request.render("sg_wechat_enterprise.Transfer", value)
|
||||
else:
|
||||
_logger.warning(u'微信企业号初始化失败.')
|
||||
return request.render('sg_wechat_enterprise.wechat_warning',
|
||||
{'title': u'警告', 'content': u'微信企业号初始化失败.'})
|
||||
|
||||
# return func(*args, **kw)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class WechatEnterprise(http.Controller):
|
||||
"""
|
||||
用于接收微信发过来的任何消息,并转发给相应的业务类进行处理
|
||||
"""
|
||||
__check_str = 'NDOEHNDSY#$_@$JFDK:Q{!'
|
||||
BASE_URL = '/we'
|
||||
|
||||
@wechat_login
|
||||
@http.route(BASE_URL + '/auth', type='http', auth='none')
|
||||
def auth(self, *args, **kw):
|
||||
"""
|
||||
企业微信免登认证
|
||||
"""
|
||||
try:
|
||||
# user_id = (request.session['uid'])
|
||||
redirect1 = kw['redirect'] if 'redirect' in kw else None
|
||||
uid = kw['user_id'] if 'redirect' in kw else None
|
||||
_logger.info('user_id %s', uid)
|
||||
if uid is not False:
|
||||
request.params['login_success'] = True
|
||||
if not redirect1:
|
||||
redirect1 = '/web'
|
||||
redirect1 = redirect1.replace('-', '&').replace('?', '#')
|
||||
logging.info('url:%s' % redirect1)
|
||||
return request.redirect(redirect1)
|
||||
except Exception as ex:
|
||||
_logger.error('无有效的登录凭证.')
|
||||
_logger.warning('auth exceptions:%s' % ex)
|
||||
return request.render('sg_wechat_enterprise.wechat_warning',
|
||||
{'title': u'警告', 'content': u'无有效的登录凭证.'})
|
||||
|
||||
@http.route('/WechatEnterprise/<string:code>/api', type='http', auth="public", methods=["GET", "POST"], csrf=False)
|
||||
def process(self, code, **kwargs):
|
||||
"""
|
||||
处理从微信服务器发送过来的请求
|
||||
:param code: 自定义代码
|
||||
:param kwargs: 包含 (msg_signature, timestamp, nonce, echostr) 等参数
|
||||
:return:
|
||||
"""
|
||||
_logger.info(u'处理从微信服务器发送过来的请求code: %s, kwargs: %s' % (code, kwargs))
|
||||
app_id = request.env['we.app'].sudo().search([('code', '=', code)], limit=1)
|
||||
if not app_id:
|
||||
_logger.warning(u'Can not find wechat app by code: {code}')
|
||||
abort(403)
|
||||
corp_id = app_id.enterprise_id
|
||||
we_chat_cpt = WXBizMsgCrypt.WXBizMsgCrypt(app_id.Token, app_id.EncodingAESKey, corp_id.corp_id)
|
||||
signature, timestamp, nonce = kwargs['msg_signature'], kwargs['timestamp'], kwargs['nonce']
|
||||
if kwargs.get('echostr'):
|
||||
echo_string = kwargs['echostr']
|
||||
sort_list = [app_id.Token, timestamp, nonce, echo_string]
|
||||
if request.env['we.tools'].sudo(). \
|
||||
check_message_signature(message_list=sort_list, msg_signature=signature):
|
||||
ret, signature_echo_string = we_chat_cpt.VerifyURL(signature, timestamp, nonce, echo_string)
|
||||
if ret == 0:
|
||||
return str(signature_echo_string, encoding="utf8")
|
||||
body_text = request.httprequest.data
|
||||
ret, signature_message = we_chat_cpt.DecryptMsg(body_text, signature, timestamp, nonce)
|
||||
xml_tree = Et.fromstring(signature_message)
|
||||
if len(xml_tree.findall("SuiteId")) > 0:
|
||||
return "success"
|
||||
if len(xml_tree.find("ApprovalInfo")) > 0:
|
||||
xmlstr = etree.fromstring(signature_message)
|
||||
# data = xml2json_from_elementtree(xmlstr)
|
||||
return request.env['we.receive.message'].sudo().sys_approval_change(xml_tree.find("ApprovalInfo"))
|
||||
data = {
|
||||
'MsgType': xml_tree.find("MsgType").text,
|
||||
# 'AgentID': xml_tree.find("AgentID").text,
|
||||
'ToUserName': xml_tree.find("ToUserName").text,
|
||||
'FromUserName': xml_tree.find("FromUserName").text,
|
||||
'CreateTime': xml_tree.find("CreateTime").text
|
||||
}
|
||||
if xml_tree.find("AgentID") != None:
|
||||
data['AgentID'] = xml_tree.find("AgentID").text
|
||||
if data["MsgType"] == "text":
|
||||
data["Content"] = xml_tree.find("Content").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "image":
|
||||
data["PicUrl"] = xml_tree.find("PicUrl").text
|
||||
data["MediaId"] = xml_tree.find("MediaId").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "voice":
|
||||
data["MediaId"] = xml_tree.find("MediaId").text
|
||||
data["Format"] = xml_tree.find("Format").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "video" or data["MsgType"] == "shortvideo":
|
||||
data["MediaId"] = xml_tree.find("MediaId").text
|
||||
data["ThumbMediaId"] = xml_tree.find("ThumbMediaId").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "location":
|
||||
data["Location_X"] = xml_tree.find("Location_X").text
|
||||
data["Location_Y"] = xml_tree.find("Location_Y").text
|
||||
data["Scale"] = xml_tree.find("Scale").text
|
||||
data["Label"] = xml_tree.find("Label").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "link":
|
||||
data["Title"] = xml_tree.find("Title").text
|
||||
data["Description"] = xml_tree.find("Description").text
|
||||
data["PicUrl"] = xml_tree.find("PicUrl").text
|
||||
data["MsgId"] = xml_tree.find("MsgId").text
|
||||
if data["MsgType"] == "event":
|
||||
if xml_tree.find("Event") == "subscribe" or xml_tree.find("Event") == "unsubscribe":
|
||||
data["Event"] = xml_tree.find("Event").text
|
||||
else:
|
||||
ret, signature_message = we_chat_cpt.EncryptMsg(signature_message, nonce, timestamp)
|
||||
return signature_message
|
||||
request.env['we.receive.message'].sudo().process_message(data)
|
||||
return ''
|
||||
|
||||
@http.route('/WechatEnterprise/transfer', type='http', auth="public", methods=["POST", "GET"], csrf=False)
|
||||
def transfer(self, url):
|
||||
value = {"url": url}
|
||||
return request.render('sg_wechat_enterprise.Transfer', value)
|
||||
@@ -1,16 +0,0 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.cron" id="ir_cron_dingtalk_accesstoken">
|
||||
<field name="name">SNS Message To 企业微信</field>
|
||||
<field name="model_id" ref="mail.model_mail_message"/>
|
||||
<field name="state">code</field>
|
||||
<field name="code">model.send_we_message()</field>
|
||||
<field name="interval_number">1</field>
|
||||
<field name="interval_type">minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="doall" eval="False"/>
|
||||
<field name="user_id" ref="base.user_root"/>
|
||||
<field name="active" eval="False"/>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,13 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
<record id="main_wechat_enterprise" model="we.config">
|
||||
<field name="name">SmartGo</field>
|
||||
<field name="corp_id">wwad4f2c227d490637</field>
|
||||
<field name="corp_secret">kq_AzJN1FoPdWjyEwAQs_cqzJhALmKhmwYMBQyJzuEs</field>
|
||||
<field name="company_id">1</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
from . import we_app
|
||||
from . import we_conf
|
||||
from . import we_send_message
|
||||
from . import we_receive_message
|
||||
from . import we_receive_message_process
|
||||
from . import res_users
|
||||
from . import we_tools
|
||||
from . import mail
|
||||
from . import client
|
||||
@@ -1,11 +0,0 @@
|
||||
from wechatpy.enterprise import WeChatClient
|
||||
|
||||
from . import api
|
||||
|
||||
|
||||
class JkmWeChatClient(WeChatClient):
|
||||
def __init__(self, corp_id, secret, session=None):
|
||||
super(JkmWeChatClient, self).__init__(corp_id, secret, session=session)
|
||||
|
||||
oauth = api.JkmWechatOauth()
|
||||
user = api.JkmWechatUser()
|
||||
@@ -1,2 +0,0 @@
|
||||
from .jkm_user import JkmWechatUser
|
||||
from .jkm_oauth import JkmWechatOauth
|
||||
@@ -1,53 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import six
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class JkmWechatOauth(BaseWeChatAPI):
|
||||
OAUTH_BASE_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize'
|
||||
|
||||
def authorize_url(self, redirect_uri, state=None, agent_id=None, scope='snsapi_base'):
|
||||
"""
|
||||
构造网页授权链接
|
||||
详情请参考
|
||||
https://work.weixin.qq.com/api/doc#90000/90135/91022
|
||||
|
||||
:param redirect_uri: 授权后重定向的回调链接地址
|
||||
:param state: 重定向后会带上 state 参数
|
||||
:param agent_id: 企业应用的id
|
||||
:param scope: 应用授权作用域
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
redirect_uri = six.moves.urllib.parse.quote(redirect_uri, safe=b'')
|
||||
url_list = [
|
||||
self.OAUTH_BASE_URL,
|
||||
'?appid=',
|
||||
self._client.corp_id,
|
||||
'&redirect_uri=',
|
||||
redirect_uri,
|
||||
'&response_type=code&scope=',
|
||||
scope,
|
||||
]
|
||||
if state:
|
||||
url_list.extend(['&state=', state])
|
||||
url_list.append('#wechat_redirect')
|
||||
if agent_id:
|
||||
url_list.extend(['&agentid=', str(agent_id)])
|
||||
return ''.join(url_list)
|
||||
|
||||
def get_user_info(self, code):
|
||||
"""
|
||||
获取访问用户身份
|
||||
详情请参考
|
||||
https://work.weixin.qq.com/api/doc#90000/90135/91023
|
||||
|
||||
:param code: 通过成员授权获取到的code
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
|
||||
return self._get(
|
||||
'user/getuserinfo',
|
||||
params={
|
||||
'code': code,
|
||||
}
|
||||
)
|
||||
@@ -1,21 +0,0 @@
|
||||
from wechatpy.enterprise.client.api import WeChatUser
|
||||
|
||||
|
||||
class JkmWechatUser(WeChatUser):
|
||||
|
||||
def get_detail(self, user_ticket):
|
||||
"""
|
||||
获取访问用户敏感信息
|
||||
详情请参考
|
||||
https://developer.work.weixin.qq.com/document/path/95833
|
||||
|
||||
:param user_ticket: 成员票据
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'user/getuserdetail',
|
||||
data={
|
||||
'user_ticket': user_ticket
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# -*- coding: utf-8-*-
|
||||
import re
|
||||
##过滤HTML中的标签
|
||||
#将HTML中标签等信息去掉
|
||||
#@param htmlstr HTML字符串.
|
||||
def filter_tags(htmlstr):
|
||||
#先过滤CDATA
|
||||
re_cdata=re.compile('//<!\[CDATA\[[^>]*//\]\]>',re.I) #匹配CDATA
|
||||
re_script=re.compile('<\s*script[^>]*>[^<]*<\s*/\s*script\s*>',re.I)#Script
|
||||
re_style=re.compile('<\s*style[^>]*>[^<]*<\s*/\s*style\s*>',re.I)#style
|
||||
re_br=re.compile('<br\s*?/?>')#处理换行
|
||||
re_h=re.compile('</?\w+[^>]*>')#HTML标签
|
||||
re_comment=re.compile('<!--[^>]*-->')#HTML注释
|
||||
s=re_cdata.sub('',htmlstr)#去掉CDATA
|
||||
s=re_script.sub('',s) #去掉SCRIPT
|
||||
s=re_style.sub('',s)#去掉style
|
||||
s=re_br.sub('\n',s)#将br转换为换行
|
||||
s=re_h.sub('',s) #去掉HTML 标签
|
||||
s=re_comment.sub('',s)#去掉HTML注释
|
||||
#去掉多余的空行
|
||||
blank_line=re.compile('\n+')
|
||||
s=blank_line.sub('\n',s)
|
||||
s=replaceCharEntity(s)#替换实体
|
||||
return s
|
||||
|
||||
##替换常用HTML字符实体.
|
||||
#使用正常的字符替换HTML中特殊的字符实体.
|
||||
#你可以添加新的实体字符到CHAR_ENTITIES中,处理更多HTML字符实体.
|
||||
#@param htmlstr HTML字符串.
|
||||
def replaceCharEntity(htmlstr):
|
||||
CHAR_ENTITIES={'nbsp':' ','160':' ',
|
||||
'lt':'<','60':'<',
|
||||
'gt':'>','62':'>',
|
||||
'amp':'&','38':'&',
|
||||
'quot':'"','34':'"',}
|
||||
|
||||
re_charEntity=re.compile(r'&#?(?P<name>\w+);')
|
||||
sz=re_charEntity.search(htmlstr)
|
||||
while sz:
|
||||
entity=sz.group()#entity全称,如>
|
||||
key=sz.group('name')#去除&;后entity,如>为gt
|
||||
try:
|
||||
htmlstr=re_charEntity.sub(CHAR_ENTITIES[key],htmlstr,1)
|
||||
sz=re_charEntity.search(htmlstr)
|
||||
except KeyError:
|
||||
#以空串代替
|
||||
htmlstr=re_charEntity.sub('',htmlstr,1)
|
||||
sz=re_charEntity.search(htmlstr)
|
||||
return htmlstr
|
||||
|
||||
def repalce(s,re_exp,repl_string):
|
||||
return re_exp.sub(repl_string,s)
|
||||
|
||||
|
||||
class MailMessage(models.Model):
|
||||
_inherit = 'mail.message'
|
||||
|
||||
we_is_send = fields.Selection([('-1', '不需要发送'), ('0', '需要发送'), ('1', '已发送')], string='订订消息发送状态', default='0',
|
||||
index=True)
|
||||
we_error_msg = fields.Char('消息不发送原因')
|
||||
|
||||
def send_we_message(self):
|
||||
"""
|
||||
定时批量发送钉钉消息
|
||||
:return:
|
||||
"""
|
||||
messages = self.search([('we_is_send', '=', '0')])
|
||||
if messages and len(messages) > 0:
|
||||
try:
|
||||
messages.send_message_to_we()
|
||||
except Exception as ex:
|
||||
pass
|
||||
|
||||
def send_message_to_we(self):
|
||||
"""
|
||||
:param agent_id: 必填,企业应用的id,整型。可在应用的设置页面查看。
|
||||
:param user_ids: 成员ID列表(消息接收者,多个接收者用‘|’分隔,最多支持1000个)。
|
||||
:param title: 标题,不超过128个字节,超过会自动截断。
|
||||
:param description: 必填,描述,不超过512个字节,超过会自动截断
|
||||
:param url: 必填,点击后跳转的链接。
|
||||
:param btntxt: 按钮文字。 默认为“详情”, 不超过4个文字,超过自动截断。
|
||||
:param party_ids: 部门ID列表。
|
||||
:param tag_ids: 标签ID列表。
|
||||
:return:
|
||||
"""
|
||||
# 获取配置信息
|
||||
config = self.env['ir.config_parameter'].sudo()
|
||||
# 获取wechat
|
||||
wechat,agent_id = self.env['we.config'].sudo().get_odoo_wechat()
|
||||
for m in self:
|
||||
we_user = m.get_we_user()
|
||||
if we_user and len(we_user)>0:
|
||||
try:
|
||||
_logger.info('wechat:%s' % wechat)
|
||||
model = self.env['ir.model'].search([('model', '=', m.model)], limit=1)
|
||||
title = "[%s] %s" % (model.name, m.record_name or m.subject or '')
|
||||
description = filter_tags(m.body) if len(filter_tags(m.body)) > 0 else '有新的状态和数据变更,请注意跟进.'
|
||||
url = '{base_url}/web#id={id}&model={model}&view_type=form'.format(
|
||||
base_url=config.get_param('web.base.url'),
|
||||
id=m.res_id,
|
||||
model=m.model)
|
||||
_logger.info('description:%s title:%s' % (description, title))
|
||||
ret = wechat.message.send_text_card(agent_id, we_user , title, description, url, btntxt='点此跟进..',
|
||||
party_ids='', tag_ids='')
|
||||
_logger.info('message_id:%s ret:%s' % (m.message_id, ret))
|
||||
m.write({'we_is_send':'1','we_error_msg':''})
|
||||
except Exception as ex:
|
||||
m.we_error_msg = '向企业微信发送消息失败! 消息编号:%s 原因:%s' % (m.message_id, ex)
|
||||
_logger.error('向企业微信发送消息失败! 消息编号:%s 原因:%s' % (m.message_id, ex))
|
||||
else:
|
||||
m.write({'we_is_send': '-1', 'we_error_msg': '无法发送,没有收件人!'})
|
||||
_logger.info('message_id:%s 不需要发送,没有收件人!' % m.message_id)
|
||||
|
||||
|
||||
return False
|
||||
@@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import random
|
||||
from odoo import models, fields, api
|
||||
from odoo.http import request
|
||||
from odoo.exceptions import AccessDenied
|
||||
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ResUsers(models.Model):
|
||||
_inherit = 'res.users'
|
||||
|
||||
we_employee_id = fields.Char(string=u'企业微信账号', default="")
|
||||
|
||||
def _check_credentials(self, we_employee_id, env):
|
||||
"""
|
||||
用户验证
|
||||
"""
|
||||
try:
|
||||
return super(ResUsers, self)._check_credentials(we_employee_id, env)
|
||||
except AccessDenied:
|
||||
user_id = self.env['res.users'].sudo().search([('we_employee_id', '=', we_employee_id)])
|
||||
if not (user_id and user_id.id == self.env.uid):
|
||||
raise
|
||||
@@ -1,213 +0,0 @@
|
||||
from odoo import api, models, exceptions, fields
|
||||
import logging
|
||||
|
||||
from odoo.http import request
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseApp(models.Model):
|
||||
_name = 'we.app'
|
||||
_description = 'Wechat Enterprise App Manage'
|
||||
|
||||
agentid = fields.Integer(string=u'应用ID', required=True)
|
||||
code = fields.Char(string=u'自定义代码')
|
||||
name = fields.Char(u'企业号应用名称')
|
||||
description = fields.Text(u'企业号应用详情')
|
||||
redirect_domain = fields.Char(u'企业应用可信域名')
|
||||
isreportuser = fields.Selection([('0', u'不接受'), ('1', u'接收')], u'变更通知', required=True, default='0')
|
||||
isreportenter = fields.Selection([('0', u'不接受'),
|
||||
('1', u'接收')], u'进入应用事件上报', required=True, default='0')
|
||||
|
||||
enterprise_id = fields.Many2one('we.config', string=u'企业微信')
|
||||
|
||||
secret = fields.Char(string=u'Secret', size=100)
|
||||
home_url = fields.Char(u'主页型应用url')
|
||||
square_logo_url = fields.Char(u'方形头像url')
|
||||
round_logo_url = fields.Char(u'圆形头像url')
|
||||
type = fields.Selection([('1', u'消息型'), ('2', u'主页型')], u'应用类型', required=True, default='1')
|
||||
allow_userinfos = fields.Char(u'企业应用可见范围(人员)')
|
||||
allow_partys = fields.Char(u'企业应用可见范围(部门)')
|
||||
allow_tags = fields.Char(u'企业应用可见范围(标签)')
|
||||
report_location_flag = fields.Selection([('0', u'不上报'), ('1', u'进入会话上报'), ('2', u'持续上报')], u'位置上报',
|
||||
required=True, default='1')
|
||||
logo_mediaid = fields.Char(u'企业应用头像的mediaid')
|
||||
close = fields.Selection([('0', u'否'), ('1', u'是')], u'是否禁用', required=True, default='0')
|
||||
app_menu_ids = fields.One2many('we.app.menu', 'agentid', string=u'自定义菜单')
|
||||
Token = fields.Char(u'Token')
|
||||
EncodingAESKey = fields.Char(u'EncodingAESKey')
|
||||
|
||||
def pull_app_from_we(self, wechat):
|
||||
"""
|
||||
从微信获取app列表
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
app_lists = wechat.agent.list()
|
||||
if app_lists:
|
||||
for app_list in app_lists:
|
||||
if 'agentid' in app_list:
|
||||
app_detail = wechat.agent.get(app_list['agentid'])
|
||||
if app_detail:
|
||||
data = {
|
||||
'agentid': str(app_detail['agentid'])
|
||||
}
|
||||
my_app = request.env["we.app"].search(
|
||||
[("agentid", "=", str(app_detail['agentid']))])
|
||||
if my_app and len(my_app) > 0:
|
||||
continue
|
||||
data['name'] = app_detail['name']
|
||||
data['square_logo_url'] = app_detail['square_logo_url']
|
||||
data['description'] = app_detail['description']
|
||||
data['close'] = str(app_detail['close'])
|
||||
data['redirect_domain'] = app_detail['redirect_domain']
|
||||
data['report_location_flag'] = str(app_detail['report_location_flag'])
|
||||
data['isreportenter'] = str(app_detail['isreportenter'])
|
||||
data['enterprise_id'] = self.id
|
||||
request.env["we.app"].create(data)
|
||||
|
||||
except Exception as e:
|
||||
raise Warning((u'获取应用列表失败,原因:%s', str(e)))
|
||||
|
||||
def push_app_to_we(self):
|
||||
"""
|
||||
同步app到微信
|
||||
:return:
|
||||
"""
|
||||
wechat = self.env['we.config'].sudo().get_wechat()
|
||||
if wechat:
|
||||
try:
|
||||
for account in self:
|
||||
app_json = {
|
||||
'name': account.name
|
||||
}
|
||||
if account.description:
|
||||
app_json['description'] = account.description
|
||||
if account.redirect_domain:
|
||||
app_json['redirect_domain'] = account.redirect_domain
|
||||
app_json['agentid'] = int(account.agentid)
|
||||
app_json['report_location_flag'] = int(account.report_location_flag)
|
||||
if account.type == "1": # 消息型应用
|
||||
if account.name and account.agentid \
|
||||
and account.isreportuser and account.isreportenter and account.report_location_flag:
|
||||
app_json['isreportuser'] = int(account.isreportuser)
|
||||
app_json['isreportenter'] = int(account.isreportenter)
|
||||
wechat.agent.set(agent_id=app_json['agentid'], name=app_json['name'],
|
||||
description=app_json['description'],
|
||||
redirect_domain=app_json['redirect_domain'],
|
||||
is_report_user=app_json['isreportuser'],
|
||||
is_report_enter=app_json['isreportenter'],
|
||||
report_location_flag=app_json['report_location_flag'])
|
||||
elif account.type == "2": # 主页型应用
|
||||
if account.name and account.agentid \
|
||||
and account.report_location_flag and account.home_url:
|
||||
app_json['home_url'] = account.home_url
|
||||
wechat.agent.set(agent_id=app_json['agentid'], name=app_json['name'],
|
||||
description=app_json['description'],
|
||||
redirect_domain=app_json['redirect_domain'],
|
||||
is_report_user=app_json['isreportuser'],
|
||||
is_report_enter=app_json['isreportenter'],
|
||||
report_location_flag=app_json['report_location_flag'])
|
||||
|
||||
except Exception as e:
|
||||
_logger.warning(u'更新app失败,原因:%s', str(e))
|
||||
raise Warning(u'更新app失败,原因:%s', str(e))
|
||||
else:
|
||||
raise exceptions.Warning(u"初始化企业号失败")
|
||||
|
||||
def update_app_menu(self):
|
||||
"""
|
||||
同步菜单至app
|
||||
:return:
|
||||
"""
|
||||
wechat = self.env['we.config'].sudo().get_wechat(agentID=self.agentid)
|
||||
menus = self.env['we.app.menu'].sudo().search([("agentid", "=", self.name)])
|
||||
wechat.menu.delete(agent_id=self.agentid)
|
||||
menu_json = {'button': []}
|
||||
button = []
|
||||
if wechat and menus:
|
||||
for menu in menus:
|
||||
menu_data = {
|
||||
'name': menu['name']
|
||||
}
|
||||
if not menu['partner_menu_id']:
|
||||
sub_menus = request.env['we.app.menu'].sudo().search(
|
||||
[("agentid", "=", self.name), ("partner_menu_id", "=", menu['name'])])
|
||||
if sub_menus and (len(sub_menus) > 0) and (len(sub_menus) < 6):
|
||||
sub_menu_list = []
|
||||
for sub_menu in sub_menus:
|
||||
sub_menu_data = {
|
||||
'name': sub_menu['name']
|
||||
}
|
||||
if menu['type'] == 'xml' or menu['type'] == 'sub_button':
|
||||
sub_menu_data['type'] = sub_menu['type']
|
||||
sub_menu_data['url'] = sub_menu['url']
|
||||
else:
|
||||
sub_menu_data['type'] = sub_menu['type']
|
||||
sub_menu_data['key'] = sub_menu['key']
|
||||
sub_menu_list.append(sub_menu_data)
|
||||
menu_data['sub_button'] = sub_menu_list
|
||||
else:
|
||||
if menu['type'] == 'xml' or menu['type'] == 'sub_button':
|
||||
menu_data['type'] = menu['type']
|
||||
menu_data['url'] = menu['url']
|
||||
else:
|
||||
menu_data['type'] = menu['type']
|
||||
menu_data['key'] = menu['key']
|
||||
button.append(menu_data)
|
||||
menu_json['button'] = button
|
||||
wechat.menu.update(agent_id=self.agentid, menu_data=menu_json)
|
||||
else:
|
||||
raise exceptions.Warning(u"初始化企业号失败或该应用无菜单")
|
||||
|
||||
|
||||
class WechatEnterpriseAppMenu(models.Model):
|
||||
_name = 'we.app.menu'
|
||||
_description = 'Wechat Enterprise App Menu Manage'
|
||||
|
||||
agentid = fields.Many2one('we.app', u'企业应用', required=True)
|
||||
partner_menu_id = fields.Many2one('we.app.menu', u'上级菜单')
|
||||
type = fields.Selection(
|
||||
[('sub_button', u'跳转至子菜单'), ('click', u'点击推事件'), ('xml', u'跳转URL'), ('scancode_push', u'扫码推事件'),
|
||||
('scancode_waitmsg', u'扫码推事件且弹出“消息接收中”提示框'),
|
||||
('pic_sysphoto', u'弹出系统拍照发图'), ('pic_photo_or_album', u'弹出拍照或者相册发图'), ('pic_weixin', u'弹出微信相册发图器'),
|
||||
('location_select', u'弹出地理位置选择器')], u'按钮的类型', required=True, default='xml')
|
||||
name = fields.Char(u'菜单标题', required=True)
|
||||
key = fields.Char(u'菜单KEY值')
|
||||
url = fields.Char(u'网页链接')
|
||||
|
||||
@api.constrains('partner_menu_id', 'name')
|
||||
def _check_menu_name_length(self):
|
||||
if self.name and self.partner_menu_id and len(self.name) > 7:
|
||||
raise Warning(u'二级菜单显示名称不能超过14个字符或7个汉字.')
|
||||
elif self.name and not self.partner_menu_id and len(self.name) > 4:
|
||||
raise Warning(u'一级菜单显示名称不能超过8个字符或4个汉字.')
|
||||
else:
|
||||
return True
|
||||
|
||||
@api.constrains('agentid')
|
||||
def check_menu_number(self):
|
||||
"""
|
||||
取得一个app的一级菜单量
|
||||
:return:
|
||||
"""
|
||||
menus_ids = self.sudo().search([('agentid', '=', self.agentid['name']), ('partner_menu_id', '=', False)])
|
||||
|
||||
if menus_ids and len(menus_ids) > 3:
|
||||
raise Warning(u'公众号的一级菜单数据不能超过3个.')
|
||||
|
||||
return True
|
||||
|
||||
@api.constrains('partner_menu_id')
|
||||
def check_submenu_number(self):
|
||||
"""
|
||||
取得一个一级菜单的子菜单数量
|
||||
:return:
|
||||
"""
|
||||
sub_menus_ids = self.sudo().search(
|
||||
[('partner_menu_id', '=', self.partner_menu_id['name']), ('partner_menu_id', '!=', False)])
|
||||
|
||||
if sub_menus_ids and len(sub_menus_ids) > 5:
|
||||
raise Warning(u'一级菜单的二级子菜单数据不能超过5个.')
|
||||
|
||||
return True
|
||||
@@ -1,62 +0,0 @@
|
||||
import logging
|
||||
import requests
|
||||
import time
|
||||
import base64
|
||||
from odoo.http import request
|
||||
from odoo import api, models, exceptions, fields, _
|
||||
from wechatpy.enterprise import WeChatClient
|
||||
from .client import JkmWeChatClient
|
||||
from json import *
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseConfigration(models.Model):
|
||||
_name = 'we.config'
|
||||
|
||||
name = fields.Char(u'名称', required=True, help=u'取个好名字吧!')
|
||||
corp_id = fields.Char(u'企业ID', required=True, help=u'企业ID,必填项')
|
||||
corp_secret = fields.Char(u'通讯录同步Secret', required=True, help=u'通讯录同步Secret,必填项')
|
||||
odoo_app_id = fields.Many2one('we.app', u'Odoo应用', help=u'在企业微信工作台配置的与Odoo进行连接的应用')
|
||||
company_id = fields.Many2one('res.company', string=u'所属公司')
|
||||
|
||||
_sql_constraints = [
|
||||
('code_complete_name_uniq', 'unique (company_id)', '一个所属公司只能定义一个企业微信!')
|
||||
]
|
||||
|
||||
def get_wechat(self, agent_id=None, company_id=1):
|
||||
"""
|
||||
取得wechat app的实例
|
||||
:param agent_id: conf or None (是企业号对象还是应用对象)
|
||||
:return:
|
||||
"""
|
||||
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
|
||||
if agent_id:
|
||||
enterprise_app = self.env['we.app'].sudo().search([('agentid', '=', agent_id)])
|
||||
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise_app.secret)
|
||||
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise.corp_secret)
|
||||
|
||||
def get_odoo_wechat(self, company_id=1):
|
||||
"""
|
||||
取得Odoo wechat app的实例
|
||||
:param agent_id: conf or None (是企业号对象还是应用对象)
|
||||
:return:
|
||||
"""
|
||||
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
|
||||
if enterprise.odoo_app_id:
|
||||
return (JkmWeChatClient(corp_id=enterprise.corp_id, secret=enterprise.odoo_app_id.secret),
|
||||
enterprise.odoo_app_id.agentid)
|
||||
else:
|
||||
raise exceptions.Warning(u'Odoo应用未配置. ')
|
||||
|
||||
def get_wechat_app(self, app_code=None, company_id=1):
|
||||
"""
|
||||
取得wechat app的实例
|
||||
:param app_code: 应用代码
|
||||
:return:
|
||||
"""
|
||||
enterprise = self.env['we.config'].sudo().search([('company_id', '=', company_id)], limit=1)
|
||||
if app_code:
|
||||
enterprise_app = self.env['we.app'].sudo().search([('code', '=', app_code)])
|
||||
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise_app.secret)
|
||||
return WeChatClient(corp_id=enterprise.corp_id, secret=enterprise.corp_secret)
|
||||
@@ -1,143 +0,0 @@
|
||||
from odoo import api, models, fields
|
||||
import logging
|
||||
from odoo.http import request
|
||||
import datetime
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseReceiveMessage(models.Model):
|
||||
_name = 'we.receive.message'
|
||||
_description = 'Wechat Enterprise Receive Message Manage'
|
||||
|
||||
name = fields.Char(required=True, index=True)
|
||||
ToUserName = fields.Char(u'企业号CorpID')
|
||||
FromUserName = fields.Char(u'成员UserID')
|
||||
CreateTime = fields.Char(u'消息创建时间')
|
||||
MsgId = fields.Char(u'消息id')
|
||||
AgentID = fields.Many2one('we.app', u'企业应用')
|
||||
MsgType = fields.Selection([('text', u'文本'),
|
||||
('voice', u'语音'),
|
||||
('image', u'图片'),
|
||||
('video', u'视频'),
|
||||
('shortvideo', u'短视频'),
|
||||
('location', u'位置'),
|
||||
('link', u'链接'),
|
||||
('subscribe', u'关注'),
|
||||
('unsubscribe', u'取消关注'),
|
||||
('xml', u'自定义菜单链接跳转'),
|
||||
('click', u'自定义菜单点击'),
|
||||
('scan', u'扫描二维码'),
|
||||
('scancode_waitmsg', u'扫描二维码并等待'),
|
||||
('event', u'取消关注')],
|
||||
string=u'消息类型', required=True, default='text')
|
||||
Content = fields.Text(u'文本消息内容')
|
||||
state = fields.Selection([('1', u'未处理'), ('2', u'已处理'), ('3', u'处理失败')], u'状态', default='1')
|
||||
PicUrl = fields.Char(u'图片链接')
|
||||
MediaId = fields.Char(u'图片媒体文件id')
|
||||
Format = fields.Char(u'语音格式')
|
||||
ThumbMediaId = fields.Char(u'视频消息缩略图的媒体id')
|
||||
Location_X = fields.Char(u'地理位置纬度')
|
||||
Location_Y = fields.Char(u'地理位置经度')
|
||||
Scale = fields.Char(u'地图缩放大小')
|
||||
Label = fields.Char(u'地理位置信息')
|
||||
Title = fields.Char(u'标题')
|
||||
Description = fields.Char(u'描述')
|
||||
Cover_PicUrl = fields.Char(u'封面缩略图的url')
|
||||
|
||||
@api.depends('MsgType', 'MsgId')
|
||||
def name_get(self):
|
||||
result = []
|
||||
for receive in self:
|
||||
name = receive.MsgType + '_' + receive.MsgId
|
||||
result.append((receive.id, name))
|
||||
return result
|
||||
|
||||
def add_message(self, data):
|
||||
"""
|
||||
增加一条待处理的上传消息
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
|
||||
app = request.env['we.app'].sudo().search([("agentid", "=", data["AgentID"])])
|
||||
receive_message_data = {
|
||||
'AgentID': app.id,
|
||||
'MsgType': data["MsgType"],
|
||||
'FromUserName': data["FromUserName"],
|
||||
'ToUserName': data["ToUserName"]
|
||||
}
|
||||
current_time = datetime.datetime.now()
|
||||
real_time = current_time + datetime.timedelta(hours=8)
|
||||
receive_message_data["CreateTime"] = real_time
|
||||
receive_message_data["name"] = data["MsgType"] + data["MsgId"]
|
||||
|
||||
if data["MsgType"] == "text":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["Content"] = data["Content"]
|
||||
if data["MsgType"] == "image":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["PicUrl"] = data["PicUrl"]
|
||||
receive_message_data["MediaId"] = data["MediaId"]
|
||||
if data["MsgType"] == "voice":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["MediaId"] = data["MediaId"]
|
||||
receive_message_data["Format"] = data["Format"]
|
||||
if data["MsgType"] == "video" or data["MsgType"] == "shortvideo":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["MediaId"] = data["MediaId"]
|
||||
receive_message_data["ThumbMediaId"] = data["ThumbMediaId"]
|
||||
if data["MsgType"] == "location":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["Location_X"] = data["Location_X"]
|
||||
receive_message_data["Location_Y"] = data["Location_Y"]
|
||||
receive_message_data["Scale"] = data["Scale"]
|
||||
receive_message_data["Label"] = data["Label"]
|
||||
if data["MsgType"] == "link":
|
||||
receive_message_data["MsgId"] = data["MsgId"]
|
||||
receive_message_data["Title"] = data["Title"]
|
||||
receive_message_data["Description"] = data["Description"]
|
||||
receive_message_data["Cover_PicUrl"] = data["PicUrl"]
|
||||
if data["MsgType"] == "event":
|
||||
if data["Event"] == "subscribe":
|
||||
receive_message_data["MsgType"] = "subscribe"
|
||||
if data["Event"] == "unsubscribe":
|
||||
receive_message_data["MsgType"] = "unsubscribe"
|
||||
|
||||
return super(WechatEnterpriseReceiveMessage, self).create(receive_message_data)
|
||||
|
||||
def process_message(self, data):
|
||||
"""
|
||||
处理未处理和失败的消息
|
||||
:param data:
|
||||
:return:
|
||||
"""
|
||||
messages = self.sudo().add_message(data)
|
||||
for message in messages:
|
||||
if message:
|
||||
if message.state == '2':
|
||||
break
|
||||
if data["MsgType"] == "text":
|
||||
process = self.env['we.receive.message.process'].get_message_process(data["MsgType"],
|
||||
data["Content"],
|
||||
data["AgentID"])
|
||||
else:
|
||||
process = self.env['we.receive.message.process'].get_message_process(data["MsgType"],
|
||||
" ",
|
||||
data["AgentID"])
|
||||
try:
|
||||
if process:
|
||||
if data["MsgType"] == "voice" or data["MsgType"] == "image" or data["MsgType"] == "video" or \
|
||||
data["MsgType"] == "shortvideo":
|
||||
process.sudo().exec_class_mothed(data["FromUserName"], data["AgentID"], data["MediaId"])
|
||||
else:
|
||||
process.sudo().exec_class_mothed(data["FromUserName"], data["AgentID"])
|
||||
else:
|
||||
return self.env['we.send.message'].sudo().send_text_message(data["FromUserName"],
|
||||
data["AgentID"],
|
||||
content=u'感谢您的关注!')
|
||||
|
||||
message.sudo().write({'state': '1'})
|
||||
except Exception as e:
|
||||
message.sudo().write({'state': u'处理失败'})
|
||||
raise Warning(u'处理失败, 原因:%s', str(e))
|
||||
@@ -1,109 +0,0 @@
|
||||
from odoo import models, fields
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseReceiveMessageProcess(models.Model):
|
||||
_name = 'we.receive.message.process'
|
||||
_description = 'Wechat Enterprise Process Receive Process Message'
|
||||
|
||||
name = fields.Char(u'名称', help=u'取个好名称方便管理,比如优惠信息索取', required=True)
|
||||
message_type = fields.Selection([('text', u'文本'),
|
||||
('voice', u'语音'),
|
||||
('image', u'图片'),
|
||||
('video', u'视频'),
|
||||
('shortvideo', u'短视频'),
|
||||
('location', u'位置'),
|
||||
('link', u'链接'),
|
||||
('subscribe', u'关注'),
|
||||
('unsubscribe', u'取消关注'),
|
||||
('xml', u'自定义菜单链接跳转'),
|
||||
('click', u'自定义菜单点击'),
|
||||
('scan', u'扫描二维码'),
|
||||
('scancode_waitmsg', u'扫描二维码并等待'),
|
||||
('unsubscribe', u'取消关注')],
|
||||
string=u'消息类型', required=True)
|
||||
message_key = fields.Char(u'关键字', required=True)
|
||||
class_name = fields.Char(u'负责处理的类', help='此处填写进行处理的类的名称),例如:topro_service_base.test', required=True, default="类的名称")
|
||||
method_name = fields.Char(u'负责处理的方法', help='此处填写进入处理的方法名,方法必须包括参数是message和account_id(微信公众号的id),这是一个dict',
|
||||
required=True, default="方法名")
|
||||
agentID = fields.Many2one('we.app', u'企业应用', required=True)
|
||||
note = fields.Text(u'备注')
|
||||
|
||||
def get_message_process(self, message_type, key_word=False, agent_id=False):
|
||||
"""
|
||||
取得消息处理的设置
|
||||
:param message_type:
|
||||
:param key_word:
|
||||
:param agent_id:
|
||||
:return:
|
||||
"""
|
||||
process = False
|
||||
if message_type:
|
||||
process = self.sudo().search(
|
||||
[('message_type', '=', message_type), ('message_key', '=', key_word), ('agentID', '=', agent_id)],
|
||||
limit=1)
|
||||
if not process and message_type and key_word:
|
||||
process = self.sudo().search([('message_type', '=', message_type), ('message_key', '=', key_word)], limit=1)
|
||||
if not process and message_type:
|
||||
process = self.sudo().search([('message_type', '=', message_type)], limit=1)
|
||||
return process
|
||||
|
||||
def exec_by_message_type(self, message_type, message_key, agent_id):
|
||||
"""
|
||||
根据消息的类型动态调用类进行执行
|
||||
:param message_type:
|
||||
:param message_key:
|
||||
:param agent_id:
|
||||
:return:
|
||||
"""
|
||||
|
||||
# 取得对象,
|
||||
if message_type and message_key:
|
||||
process = self.get_message_process(message_type, message_key, agent_id)
|
||||
process.sudo().exec_class_mothed(message_key, agent_id)
|
||||
|
||||
def exec_class_mothed(self, from_user_name, agent_id, media_id=None):
|
||||
"""
|
||||
执行类的方法
|
||||
:param from_user_name:
|
||||
:param agent_id:
|
||||
:param media_id:
|
||||
:return:
|
||||
"""
|
||||
|
||||
_logger.debug('exec_class_mothed')
|
||||
object_function = getattr(self.env[self.class_name], self.method_name)
|
||||
ret = object_function(from_user_name, agent_id, media_id)
|
||||
|
||||
return ret
|
||||
|
||||
def hello(self, from_user_name, agent_id):
|
||||
"""
|
||||
demo方法,模拟动态处理客户的需求
|
||||
:param from_user_name:
|
||||
:param agent_id:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
self.env['we.send.message'].sudo(). \
|
||||
send_news_message(from_user_name, agent_id, u'测试图文信息', u'测试图文信息的描述',
|
||||
'http://www.baidu.com',
|
||||
'http://www.kia-hnsyt.com.cn/uploads/allimg/141204/1-1412041624240-L.jpg')
|
||||
except Exception as e:
|
||||
_logger.warning(u'发送微信文本失败原因:%s', str(e))
|
||||
raise Warning(str(e))
|
||||
|
||||
def send_img(self, from_user_name, agent_id):
|
||||
"""
|
||||
demo方法,模拟动态处理客户的需求
|
||||
:param from_user_name:
|
||||
:param agent_id:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
self.env['we.send.message'].sudo().send_text_message(from_user_name, agent_id, u'即将为您发送一条消息')
|
||||
except Exception as e:
|
||||
_logger.warning(u'发送微信文本失败原因:%s', str(e))
|
||||
raise Warning(str(e))
|
||||
@@ -1,108 +0,0 @@
|
||||
from odoo import api, models, exceptions, fields
|
||||
import logging
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseSendMessage(models.Model):
|
||||
_name = 'we.send.message'
|
||||
_description = 'Wechat Enterprise Send Message Manage'
|
||||
|
||||
touser = fields.Many2many('res.users', 'send_message_to_users_ref',
|
||||
'wechat_contacts_id', 'touser', u'成员列表')
|
||||
msgtype = fields.Selection([('text', u'文字消息'), ('image', u'图片消息'), ('voice', u'语音消息'), ('video', u'视频消息'),
|
||||
('file', u'文件消息'), ('news', u'图文消息'), ('mpnews', u'微信后台图文消息')], u'消息类型',
|
||||
required=True, default='text')
|
||||
agentid = fields.Many2one('we.app', u'发送消息的企业应用', required=True)
|
||||
content = fields.Char(u'消息内容')
|
||||
media_id = fields.Char(u'媒体文件')
|
||||
title = fields.Char(u'标题')
|
||||
description = fields.Text(u'描述')
|
||||
articles = fields.Char(u'图文消息')
|
||||
url = fields.Char(u'点击后跳转的链接')
|
||||
picurl = fields.Char(u'图文消息的图片链接')
|
||||
thumb_media_id = fields.Char(u'图文消息缩略图')
|
||||
author = fields.Char(u'图文消息的作者')
|
||||
content_source_url = fields.Char(u'图文消息点击“阅读原文”之后的页面链接')
|
||||
news_content = fields.Char(u'图文消息的内容,支持html标签')
|
||||
digest = fields.Char(u'图文消息的描述')
|
||||
show_cover_pic = fields.Selection([('0', u'否'), ('1', u'是')], u'是否显示封面', default='0')
|
||||
safe = fields.Selection([('0', u'否'), ('1', u'是')], u'是否是保密消息', required=True, default='0')
|
||||
|
||||
def send_message(self):
|
||||
"""
|
||||
发送消息给关注企业号的用户
|
||||
:return:
|
||||
"""
|
||||
users = ""
|
||||
i = 0
|
||||
if self.touser and len(self.touser) > 0:
|
||||
for data in self.touser:
|
||||
i = i + 1
|
||||
if i == len(self.touser):
|
||||
if data['we_employee_id']:
|
||||
users = users + data['we_employee_id']
|
||||
else:
|
||||
if data['we_employee_id']:
|
||||
users = users + data['we_employee_id'] + "|"
|
||||
if users == "":
|
||||
users = '@all'
|
||||
partys = ""
|
||||
|
||||
if self.msgtype == "news":
|
||||
self.send_news_message(users, self.agentid['agentid'], self.title, self.description, self.url, self.picurl)
|
||||
elif self.msgtype == "text":
|
||||
self.send_text_message(users, self.agentid['agentid'], self.content, partys)
|
||||
|
||||
def send_text_message(self, userid, agentid, content, partyid=None):
|
||||
"""
|
||||
发送文本消息给关注企业号的用户
|
||||
:param userid:
|
||||
:param agentid:
|
||||
:param content:
|
||||
:param partyid:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
wechat = self.env['we.config'].sudo().get_wechat(agent_id=agentid)
|
||||
if wechat:
|
||||
data = {
|
||||
'safe': "0",
|
||||
'msgtype': 'text',
|
||||
'agentid': agentid,
|
||||
'touser': userid,
|
||||
'toparty': partyid,
|
||||
'content': content
|
||||
}
|
||||
wechat.message.send_text(agent_id=data['agentid'], user_ids=data['touser'], content=data['content'],
|
||||
party_ids=data['toparty'], safe=data['safe'])
|
||||
else:
|
||||
raise exceptions.Warning(u"初始化企业号失败")
|
||||
except Exception as e:
|
||||
logging.error('send_text_message:%s' % e)
|
||||
|
||||
# 发送图文消息给关注企业号的用户
|
||||
def send_news_message(self, userid, agentid, title=None, description=None, url=None, picurl=None):
|
||||
"""
|
||||
发送图文消息给关注企业号的用户
|
||||
:param userid:
|
||||
:param agentid:
|
||||
:param title:
|
||||
:param description:
|
||||
:param url:
|
||||
:param picurl:
|
||||
:return:
|
||||
"""
|
||||
wechat = self.env['we.config'].sudo().get_wechat(agent_id=agentid)
|
||||
if wechat:
|
||||
articles = [
|
||||
{
|
||||
'url': url,
|
||||
'image': picurl,
|
||||
'description': description,
|
||||
'title': title
|
||||
}
|
||||
]
|
||||
wechat.message.send_articles(agent_id=agentid, user_ids=userid, articles=articles)
|
||||
else:
|
||||
raise exceptions.Warning(u"初始化企业号失败")
|
||||
@@ -1,89 +0,0 @@
|
||||
from odoo import api, models, exceptions
|
||||
from odoo.http import request
|
||||
import logging
|
||||
import hashlib
|
||||
import base64
|
||||
import time
|
||||
import requests
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WechatEnterpriseTools(models.Model):
|
||||
"""
|
||||
微信企业号工具类
|
||||
"""
|
||||
_name = 'we.tools'
|
||||
_description = '微信企业号工具类'
|
||||
|
||||
def get_media(self, media_id):
|
||||
"""
|
||||
通过media_id 获取媒体文件
|
||||
:param media_id: media id
|
||||
:return:
|
||||
"""
|
||||
wechat = self.env['we.config'].sudo().get_wechat()
|
||||
try:
|
||||
media = wechat.media.download(media_id=media_id)
|
||||
return {
|
||||
'errcode': 0,
|
||||
'errmsg': 'ok',
|
||||
'media': media.content
|
||||
}
|
||||
except Exception as ex:
|
||||
_logger.info(u'get media fail, message: {str(ex)}.')
|
||||
return {
|
||||
'errcode': 30001,
|
||||
'errmsg': str(ex)
|
||||
}
|
||||
|
||||
def get_jsapi_ticket(self):
|
||||
"""
|
||||
获取jsapi_ticket
|
||||
:return:
|
||||
"""
|
||||
if request.session.get('ticket') and request.session.get('ticket_time') \
|
||||
and int(time.time()) - request.session['ticket_time'] <= 7000:
|
||||
return {
|
||||
'errcode': 0,
|
||||
'errmsg': 'ok',
|
||||
'ticket': request.session['ticket']
|
||||
}
|
||||
wechat = self.env['we.config'].sudo().get_wechat()
|
||||
get_token = wechat.fetch_access_token()
|
||||
if get_token['errcode'] == 0 and get_token['errmsg'] == 'ok':
|
||||
access_token = get_token['access_token']
|
||||
url = u'https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token={access_token}'
|
||||
response = requests.get(url).json()
|
||||
if response['errcode'] == 0 and response['errmsg'] == 'ok':
|
||||
request.session['ticket'] = response['ticket']
|
||||
request.session['ticket_time'] = int(time.time())
|
||||
return response
|
||||
return {
|
||||
"errcode": 10002,
|
||||
"errmsg": "get ticket fail."
|
||||
}
|
||||
|
||||
def check_message_signature(self, message_list, msg_signature):
|
||||
"""
|
||||
校验消息的正确性
|
||||
:param message_list: 消息列表 (list: token, timestamp, nonce, echo_string)
|
||||
:param msg_signature: 签名
|
||||
:return: true or false
|
||||
"""
|
||||
_logger.info(u'check message signature.')
|
||||
message_list.sort()
|
||||
message_str = "".join(message_list)
|
||||
sha1_message = hashlib.sha1(str(message_str).encode('utf-8')).hexdigest()
|
||||
if sha1_message == msg_signature:
|
||||
return True
|
||||
return False
|
||||
|
||||
def decode_echo_str(self, echo_str):
|
||||
"""
|
||||
解密echo string 得到明文内容
|
||||
:param echo_str: 加密字符串
|
||||
:return: message
|
||||
"""
|
||||
_logger.info(u'decode echo string.')
|
||||
base64_str = base64.b64decode(echo_str)
|
||||
@@ -1 +0,0 @@
|
||||
wechatpy==1.8.6
|
||||
@@ -1,13 +0,0 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
|
||||
access_user_we_app,we_app,model_we_app,,1,1,1,1
|
||||
|
||||
access_user_we_config,we_config,model_we_config,,1,1,1,1
|
||||
|
||||
access_user_we_receive_message,we_receive_message,model_we_receive_message,,1,1,1,1
|
||||
|
||||
access_user_we_receive_message_process,we_receive_message_process,model_we_receive_message_process,,1,1,1,1
|
||||
|
||||
access_user_we_send_message,we_send_message,model_we_send_message,,1,1,1,1
|
||||
|
||||
access_user_we_app_menu,we_app_menu,model_we_app_menu,,1,1,1,1
|
||||
|
@@ -1,24 +0,0 @@
|
||||
@-webkit-keyframes loadingCircle {
|
||||
to {
|
||||
transform:rotate(1turn)
|
||||
}
|
||||
}
|
||||
@keyframes loadingCircle {
|
||||
to {
|
||||
transform:rotate(1turn)
|
||||
}
|
||||
}
|
||||
.load-box {
|
||||
position:fixed;
|
||||
top:calc(50% - 11px);
|
||||
left:calc(50% - 111px);
|
||||
display:flex;
|
||||
align-items:center;
|
||||
}
|
||||
.load-icon {
|
||||
animation:loadingCircle 1s linear infinite;
|
||||
}
|
||||
.load-txt {
|
||||
font-size:16px;
|
||||
margin-left:10px;
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.3 KiB |
@@ -1,26 +0,0 @@
|
||||
/**
|
||||
* Created by jiangxiang on 2016/3/14.
|
||||
*/
|
||||
|
||||
//<2F><>ȡ<EFBFBD><C8A1><EFBFBD>Ӵ<EFBFBD><D3B4><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>openID<49><44><EFBFBD><EFBFBD>
|
||||
function getUrlParam(url, name) {
|
||||
var pattern = new RegExp("[?&]" + name + "\=([^&]+)", "g");
|
||||
var matcher = pattern.exec(url);
|
||||
var items = null;
|
||||
if (matcher != null) {
|
||||
try {
|
||||
items = decodeURIComponent(decodeURIComponent(matcher[1]));
|
||||
} catch (e) {
|
||||
try {
|
||||
items = decodeURIComponent(matcher[1]);
|
||||
} catch (e) {
|
||||
items = matcher[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
items = items.replace(/^\s*/, "");
|
||||
return items;
|
||||
}
|
||||
|
||||
var url = document.getElementById("url").innerText;
|
||||
window.location.href = url;
|
||||
@@ -1 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
@@ -1,84 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="view_wechat_enterprise_app_form">
|
||||
<field name="name">we.app.form</field>
|
||||
<field name="model">we.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="应用配置">
|
||||
<!--<header>-->
|
||||
<!--<button string="同步当前应用菜单至微信" name="update_app_menu" type="object" class="oe_highlight"/>-->
|
||||
<!--</header>-->
|
||||
<sheet>
|
||||
<group col="4" string="基础信息">
|
||||
<field name="name"/>
|
||||
<field name="enterprise_id" required="1"/>
|
||||
<field name="agentid"/>
|
||||
<field name="type"/>
|
||||
</group>
|
||||
|
||||
<group col="4" string="配置信息">
|
||||
<field name="code"/>
|
||||
<field name="Token"/>
|
||||
<field name="EncodingAESKey"/>
|
||||
<field name="secret" required="1"/>
|
||||
</group>
|
||||
|
||||
<group col="4" string="上报信息设置">
|
||||
<field name="report_location_flag"/>
|
||||
<field name="isreportuser" attrs="{'invisible': [('type','in',('2'))]}"/>
|
||||
<field name="close"/>
|
||||
<field name="isreportenter" attrs="{'invisible': [('type','in',('2'))]}"/>
|
||||
</group>
|
||||
|
||||
<group col="4" string="更多">
|
||||
<field name="redirect_domain"/>
|
||||
<field name="description"/>
|
||||
<field name="home_url" attrs="{'invisible': [('type','in',('1'))]}"/>
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- tree -->
|
||||
<record id="view_wechat_enterprise_app_tree" model="ir.ui.view">
|
||||
<field name="name">we.app.tree</field>
|
||||
<field name="model">we.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="应用配置">
|
||||
<field name="agentid"/>
|
||||
<field name="name"/>
|
||||
<field name='type'/>
|
||||
<field name="redirect_domain"/>
|
||||
<field name="description"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- search -->
|
||||
<record id="view_wechat_enterprise_app_search" model="ir.ui.view">
|
||||
<field name="name">we.app.search</field>
|
||||
<field name="model">we.app</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="应用配置">
|
||||
<field name="name" filter_domain="[('name','ilike',self)]" string="应用配置"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- action -->
|
||||
<record id="view_wechat_enterprise_app_action" model="ir.actions.act_window">
|
||||
<field name="name">应用配置</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">we.app</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="view_id" ref="view_wechat_enterprise_app_tree"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<!-- View -->
|
||||
<record id="view_mail_message_form_dingtalk" model="ir.ui.view">
|
||||
<field name="name">mail.message.form.dingtalk</field>
|
||||
<field name="model">mail.message</field>
|
||||
<field name="inherit_id" ref="mail.view_message_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<notebook position="inside">
|
||||
<page string="企业微信">
|
||||
<group>
|
||||
<field name="we_is_send"/>
|
||||
<field name="we_error_msg"/>
|
||||
|
||||
</group>
|
||||
<footer class="modal-footer">
|
||||
<button type="object" name="send_message_to_we" string="发送到企业微信"/>
|
||||
</footer>
|
||||
</page>
|
||||
</notebook>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
@@ -1,34 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem
|
||||
id="menu_wechat_enterprise"
|
||||
name="企业微信" parent="base.menu_administration"
|
||||
sequence="6"/>
|
||||
<menuitem id="menu_wechat_enterprise_setup" name="设置" sequence="120" parent="menu_wechat_enterprise"/>
|
||||
<menuitem action="view_wechat_enterprise_config_action" id="menu_wechat_enterprise_config_form" name="企业微信"
|
||||
parent="menu_wechat_enterprise_setup" sequence="20"/>
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_app_action" id="menu_wechat_enterprise_app_form" name="应用列表"
|
||||
parent="menu_wechat_enterprise_setup" sequence="20"/>
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_app_menu_action"
|
||||
id="menu_wechat_enterprise_app_menu_form" name="应用菜单"
|
||||
parent="menu_wechat_enterprise_setup" sequence="30"/>
|
||||
|
||||
<menuitem id="menu_wechat_enterprise_process" name="业务处理" sequence="100" parent="menu_wechat_enterprise"/>
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_receive_message_process_action"
|
||||
id="menu_wechat_enterprise_receive_message_process_form" name="接收消息处理"
|
||||
parent="menu_wechat_enterprise_process" sequence="30"/>
|
||||
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_receive_message_action"
|
||||
id="menu_wechat_enterprise_receive_message_form" name="接收消息"
|
||||
parent="menu_wechat_enterprise_process" sequence="30"/>
|
||||
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_send_message_action"
|
||||
id="menu_wechat_enterprise_send_message_form" name="发送消息"
|
||||
parent="menu_wechat_enterprise_process" sequence="30"/>
|
||||
|
||||
</odoo>
|
||||
@@ -1,20 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="view_users_account_form" model="ir.ui.view">
|
||||
<field name="name">res.users.account.form</field>
|
||||
<field name="model">res.users</field>
|
||||
<field name="inherit_id" ref="base.view_users_form"/>
|
||||
<field name="arch" type="xml">
|
||||
<page name="preferences" position="after">
|
||||
<page name="account" string="企业微信">
|
||||
<group>
|
||||
<field name="we_employee_id"/>
|
||||
</group>
|
||||
</page>
|
||||
</page>
|
||||
</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="view_wechat_enterprise_app_form">
|
||||
<field name="name">we.app.form</field>
|
||||
<field name="model">we.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="应用配置">
|
||||
<!--<header>-->
|
||||
<!--<button string="同步当前应用菜单至微信" name="update_app_menu" type="object" class="oe_highlight"/>-->
|
||||
<!--</header>-->
|
||||
<sheet>
|
||||
<group col="4" string="基础信息">
|
||||
<field name="name"/>
|
||||
<field name="enterprise_id" required="1"/>
|
||||
<field name="agentid"/>
|
||||
<field name="type"/>
|
||||
</group>
|
||||
|
||||
<group col="4" string="配置信息">
|
||||
<field name="code"/>
|
||||
<field name="Token"/>
|
||||
<field name="EncodingAESKey"/>
|
||||
<field name="secret" required="1"/>
|
||||
</group>
|
||||
|
||||
<group col="4" string="上报信息设置">
|
||||
<field name="report_location_flag"/>
|
||||
<field name="isreportuser" attrs="{'invisible': [('type','in',('2'))]}"/>
|
||||
<field name="close"/>
|
||||
<field name="isreportenter" attrs="{'invisible': [('type','in',('2'))]}"/>
|
||||
</group>
|
||||
|
||||
<group col="4" string="更多">
|
||||
<field name="redirect_domain"/>
|
||||
<field name="description"/>
|
||||
<field name="home_url" attrs="{'invisible': [('type','in',('1'))]}"/>
|
||||
</group>
|
||||
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- tree -->
|
||||
<record id="view_wechat_enterprise_app_tree" model="ir.ui.view">
|
||||
<field name="name">we.app.tree</field>
|
||||
<field name="model">we.app</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="应用配置">
|
||||
<field name="agentid"/>
|
||||
<field name="name"/>
|
||||
<field name='type'/>
|
||||
<field name="redirect_domain"/>
|
||||
<field name="description"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- search -->
|
||||
<record id="view_wechat_enterprise_app_search" model="ir.ui.view">
|
||||
<field name="name">we.app.search</field>
|
||||
<field name="model">we.app</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="应用配置">
|
||||
<field name="name" filter_domain="[('name','ilike',self)]" string="应用配置"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- action -->
|
||||
<record id="view_wechat_enterprise_app_action" model="ir.actions.act_window">
|
||||
<field name="name">应用配置</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">we.app</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="view_id" ref="view_wechat_enterprise_app_tree"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="view_wechat_enterprise_app_menu_form">
|
||||
<field name="name">we.app.menu.form</field>
|
||||
<field name="model">we.app.menu</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="应用菜单">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="agentid"/>
|
||||
<field name="type"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="partner_menu_id"
|
||||
attrs="{'invisible': [('type','in',('sub_button'))]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="key" attrs="{'invisible': [('type','in',('view','sub_button'))]}"/>
|
||||
<field name="url"
|
||||
attrs="{'invisible': [('type','in',('sub_button','click','scancode_push','scancode_waitmsg','pic_sysphoto','pic_photo_or_album','video'))]}"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- tree -->
|
||||
<record id="view_wechat_enterprise_app_menu_tree" model="ir.ui.view">
|
||||
<field name="name">we.app.menu.tree</field>
|
||||
<field name="model">we.app.menu</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="应用菜单">
|
||||
<field name="agentid"/>
|
||||
<field name="name"/>
|
||||
<field name="partner_menu_id"/>
|
||||
<field name="type"/>
|
||||
<field name="key"/>
|
||||
<field name="url"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- search -->
|
||||
<record id="view_wechat_enterprise_app_menu_search" model="ir.ui.view">
|
||||
<field name="name">we.app.menu.search</field>
|
||||
<field name="model">we.app.menu</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="应用菜单">
|
||||
<field name="name" filter_domain="[('name','ilike',self)]" string="菜单名称"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- action -->
|
||||
<record id="view_wechat_enterprise_app_menu_action" model="ir.actions.act_window">
|
||||
<field name="name">应用菜单</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">we.app.menu</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="view_id" ref="view_wechat_enterprise_app_menu_tree"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="view_wechat_enterprise_config_form">
|
||||
<field name="name">we.config.form</field>
|
||||
<field name="model">we.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="企业微信">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="company_id" required="1" options="{'no_create_edit': True, 'no_create': True}"/>
|
||||
<field name="corp_id"/>
|
||||
<field name="corp_secret"/>
|
||||
<field name="odoo_app_id"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- tree -->
|
||||
<record id="view_wechat_enterprise_config_tree" model="ir.ui.view">
|
||||
<field name="name">we.config.tree</field>
|
||||
<field name="model">we.config</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="企业微信">
|
||||
<field name="name"/>
|
||||
<field name='corp_secret'/>
|
||||
<field name="corp_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- action -->
|
||||
<record id="view_wechat_enterprise_config_action" model="ir.actions.act_window">
|
||||
<field name="name">企业微信</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">we.config</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<menuitem id="menu_wechat_enterprise" name="企业微信" sequence="20" parent="base.menu_administration"
|
||||
web_icon="sg_wechat_enterprise,static/description/qyh.png">
|
||||
<menuitem id="menu_wechat_enterprise_setup" name="设置" sequence="120">
|
||||
<menuitem action="view_wechat_enterprise_config_action" id="menu_wechat_enterprise_config_form" name="企业微信"
|
||||
parent="menu_wechat_enterprise_setup" sequence="20"/>
|
||||
<menuitem action="view_wechat_enterprise_app_action" id="menu_wechat_enterprise_app_form" name="应用列表"
|
||||
sequence="20"/>
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_app_action" id="menu_wechat_enterprise_app_form" name="应用列表"
|
||||
parent="menu_wechat_enterprise_setup" sequence="20"/>
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_app_menu_action"
|
||||
id="menu_wechat_enterprise_app_menu_form" name="应用菜单"
|
||||
parent="menu_wechat_enterprise_setup" sequence="30"/>
|
||||
</menuitem>
|
||||
<menuitem id="menu_wechat_enterprise_contact" name="通讯录" sequence="110"/>
|
||||
|
||||
<menuitem id="menu_wechat_enterprise_process" name="业务处理" sequence="100">
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_receive_message_process_action"
|
||||
id="menu_wechat_enterprise_receive_message_process_form" name="接收消息处理"
|
||||
parent="menu_wechat_enterprise_process" sequence="30"/>
|
||||
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_receive_message_action"
|
||||
id="menu_wechat_enterprise_receive_message_form" name="接收消息"
|
||||
parent="menu_wechat_enterprise_process" sequence="30"/>
|
||||
|
||||
<!-- menu -->
|
||||
<menuitem action="view_wechat_enterprise_send_message_action"
|
||||
id="menu_wechat_enterprise_send_message_form" name="发送消息"
|
||||
parent="menu_wechat_enterprise_process" sequence="30"/>
|
||||
</menuitem>
|
||||
</menuitem>
|
||||
|
||||
</odoo>
|
||||
@@ -1,70 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="view_wechat_enterprise_receive_message_process_form">
|
||||
<field name="name">we.receive.message.process.form</field>
|
||||
<field name="model">we.receive.message.process</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="接收消息处理">
|
||||
<sheet>
|
||||
<group>
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="message_key"/>
|
||||
<field name="class_name"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="message_type"/>
|
||||
<field name="agentID"/>
|
||||
<field name="method_name"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="note"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- tree -->
|
||||
<record id="view_wechat_enterprise_receive_message_process_tree" model="ir.ui.view">
|
||||
<field name="name">we.receive.message.process.tree</field>
|
||||
<field name="model">we.receive.message.process</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="接收消息处理">
|
||||
<field name="name"/>
|
||||
<field name="message_key"/>
|
||||
<field name="class_name"/>
|
||||
<field name="method_name"/>
|
||||
<field name="message_type"/>
|
||||
<field name="agentID"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- search -->
|
||||
<record id="view_wechat_enterprise_receive_message_process_search" model="ir.ui.view">
|
||||
<field name="name">we.receive.message.process.search</field>
|
||||
<field name="model">we.receive.message.process</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="接收消息处理">
|
||||
<field name="message_key" filter_domain="[('message_key','ilike',self)]" string="关键字"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- action -->
|
||||
<record id="view_wechat_enterprise_receive_message_process_action" model="ir.actions.act_window">
|
||||
<field name="name">接收消息处理</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">we.receive.message.process</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="view_id" ref="view_wechat_enterprise_receive_message_process_tree"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="view_wechat_enterprise_receive_message_form">
|
||||
<field name="name">we.receive.message.form</field>
|
||||
<field name="model">we.receive.message</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="接收消息">
|
||||
<sheet>
|
||||
<group string="消息基本信息">
|
||||
<group>
|
||||
<field name="ToUserName"/>
|
||||
<field name="AgentID"/>
|
||||
<field name="MsgId"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="FromUserName"/>
|
||||
<field name="state"/>
|
||||
<field name="CreateTime"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="MsgType"/>
|
||||
</group>
|
||||
<group string="消息内容">
|
||||
<field name="MediaId" attrs="{'invisible': [('MsgType','in',('text','location','link'))]}"/>
|
||||
<field name="Format" attrs="{'invisible': [('MsgType','in',('text','image','video','shortvideo','location','link'))]}"/>
|
||||
<field name="ThumbMediaId" attrs="{'invisible': [('MsgType','in',('text','image','voice','location','link'))]}"/>
|
||||
<field name="PicUrl" attrs="{'invisible': [('MsgType','in',('text','voice','video','shortvideo','location','link'))]}"/>
|
||||
<field name="Content" attrs="{'invisible': [('MsgType','in',('image','voice','video','location','link','shortvideo'))]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="Location_X" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','link'))]}"/>
|
||||
<field name="Scale" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','link'))]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="Location_Y" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','link'))]}"/>
|
||||
<field name="Label" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','link'))]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="Title" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','location'))]}"/>
|
||||
<field name="Description" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','location'))]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="Cover_PicUrl" attrs="{'invisible': [('MsgType','in',('text','image','voice','video','shortvideo','location'))]}"/>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- tree -->
|
||||
<record id="view_wechat_enterprise_receive_message_tree" model="ir.ui.view">
|
||||
<field name="name">we.receive.message.tree</field>
|
||||
<field name="model">we.receive.message</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="接收消息">
|
||||
<field name="ToUserName"/>
|
||||
<field name="CreateTime"/>
|
||||
<field name="FromUserName"/>
|
||||
<field name="MsgId"/>
|
||||
<field name="AgentID"/>
|
||||
<field name="MsgType"/>
|
||||
<field name="Content"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- search -->
|
||||
<record id="view_wechat_enterprise_receive_message_search" model="ir.ui.view">
|
||||
<field name="name">we.receive.message.search</field>
|
||||
<field name="model">we.receive.message</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="接收消息">
|
||||
<field name="Content" filter_domain="[('Content','ilike',self)]" string="消息内容"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- action -->
|
||||
<record id="view_wechat_enterprise_receive_message_action" model="ir.actions.act_window">
|
||||
<field name="name">接收消息</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">we.receive.message</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="view_id" ref="view_wechat_enterprise_receive_message_tree"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.ui.view" id="view_wechat_enterprise_send_message_form">
|
||||
<field name="name">we.send.message.form</field>
|
||||
<field name="model">we.send.message</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="发送消息">
|
||||
<header>
|
||||
<button string="发送本条消息" name="send_message" type="object" class="oe_highlight"/>
|
||||
</header>
|
||||
<sheet>
|
||||
<group string="消息内容">
|
||||
<group>
|
||||
<field name="msgtype"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="agentid"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<group>
|
||||
<field name="content"
|
||||
attrs="{'invisible': [('msgtype','in',('image','voice','video','file','news','mpnews'))]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="media_id"
|
||||
attrs="{'invisible': [('msgtype','in',('text','news','mpnews'))]}"/>
|
||||
</group>
|
||||
</group>
|
||||
<group>
|
||||
<field name="title"
|
||||
attrs="{'invisible': [('msgtype','in',('image','voice','text','file'))]}"/>
|
||||
<field name="description"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','mpnews'))]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="url"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','mpnews','video'))]}"/>
|
||||
<field name="picurl"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','mpnews','video'))]}"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="thumb_media_id"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
|
||||
<field name="author"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
|
||||
<field name="content_source_url"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
|
||||
<field name="news_content"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
|
||||
<field name="digest"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
|
||||
<field name="show_cover_pic"
|
||||
attrs="{'invisible': [('msgtype','in',('text','image','voice','file','news','video'))]}"/>
|
||||
</group>
|
||||
<group string="消息发送的范围(若不填,则默认发送全体员工)">
|
||||
<field name="touser"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- tree -->
|
||||
<record id="view_wechat_enterprise_send_message_tree" model="ir.ui.view">
|
||||
<field name="name">we.send.message.tree</field>
|
||||
<field name="model">we.send.message</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="发送消息">
|
||||
<field name="touser"/>
|
||||
<field name="msgtype"/>
|
||||
<field name="agentid"/>
|
||||
<field name="content"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- search -->
|
||||
<record id="view_wechat_enterprise_send_message_search" model="ir.ui.view">
|
||||
<field name="name">we.send.message.search</field>
|
||||
<field name="model">we.send.message</field>
|
||||
<field name="mode">primary</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="发送消息">
|
||||
<field name="content" filter_domain="[('content','ilike',self)]" string="文字消息内容"/>
|
||||
<field name="title" filter_domain="[('title','ilike',self)]" string="图文消息标题"/>
|
||||
<field name="description" filter_domain="[('description','ilike',self)]" string="图文消息描述"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<!-- action -->
|
||||
<record id="view_wechat_enterprise_send_message_action" model="ir.actions.act_window">
|
||||
<field name="name">发送消息</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">we.send.message</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="binding_view_types">form</field>
|
||||
<field name="view_id" ref="view_wechat_enterprise_send_message_tree"/>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<odoo>
|
||||
<data>
|
||||
<template id="sg_wechat_enterprise.layout" name="Wechat_Enterprise_Layout">
|
||||
<!DOCTYPE html>
|
||||
<html style="height:100%">
|
||||
<head>
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
|
||||
<t t-raw="head or ''"/>
|
||||
</head>
|
||||
<body>
|
||||
<t t-raw="0"/>
|
||||
</body>
|
||||
</html>
|
||||
</template>
|
||||
|
||||
<template id="sg_wechat_enterprise.Transfer" name="Wechat_Enterprise_Transfer">
|
||||
<t t-call="sg_wechat_enterprise.layout">
|
||||
<t t-set="head">
|
||||
</t>
|
||||
<p id="url" style="display:none">
|
||||
<t t-raw="url" />
|
||||
</p>
|
||||
<div class="load-box">
|
||||
<svg class="load-icon" viewBox="0 0 1024 1024" data-icon="loading" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false"><path d="M988 548c-19.9 0-36-16.1-36-36 0-59.4-11.6-117-34.6-171.3a440.45 440.45 0 0 0-94.3-139.9 437.71 437.71 0 0 0-139.9-94.3C629 83.6 571.4 72 512 72c-19.9 0-36-16.1-36-36s16.1-36 36-36c69.1 0 136.2 13.5 199.3 40.3C772.3 66 827 103 874 150c47 47 83.9 101.8 109.7 162.7 26.7 63.1 40.2 130.2 40.2 199.3.1 19.9-16 36-35.9 36z"></path></svg>
|
||||
<span class="load-txt">页面正在加载中,请耐心等候。</span>
|
||||
</div>
|
||||
<link rel="stylesheet" href="/sg_wechat_enterprise/static/css/loading.css"/>
|
||||
<script src="/sg_wechat_enterprise/static/js/url_transfers.js"></script>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
<template id="sg_wechat_enterprise.wechat_warning" name="Wechat_Enterprise_Warning">
|
||||
<t t-call="sg_wechat_enterprise.layout">
|
||||
<t t-set="head">
|
||||
</t>
|
||||
<div class="mr_center">
|
||||
<p><h3>[<t t-esc="title"/>]</h3></p>
|
||||
<p><t t-esc="content" /></p>
|
||||
</div>
|
||||
</t>
|
||||
</template>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -1,33 +0,0 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import logging
|
||||
try:
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
except ImportError:
|
||||
from pkg_resources import declare_namespace
|
||||
declare_namespace(__name__)
|
||||
|
||||
from wechatpy.parser import parse_message # NOQA
|
||||
from wechatpy.replies import create_reply # NOQA
|
||||
from wechatpy.client import WeChatClient # NOQA
|
||||
from wechatpy.exceptions import WeChatException # NOQA
|
||||
from wechatpy.exceptions import WeChatClientException # NOQA
|
||||
from wechatpy.oauth import WeChatOAuth # NOQA
|
||||
from wechatpy.exceptions import WeChatOAuthException # NOQA
|
||||
from wechatpy.pay import WeChatPay # NOQA
|
||||
from wechatpy.exceptions import WeChatPayException # NOQA
|
||||
from wechatpy.component import WeChatComponent # NOQA
|
||||
|
||||
|
||||
__version__ = '1.3.1'
|
||||
__author__ = 'messense'
|
||||
|
||||
# Set default logging handler to avoid "No handler found" warnings.
|
||||
try: # Python 2.7+
|
||||
from logging import NullHandler
|
||||
except ImportError:
|
||||
class NullHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
logging.getLogger(__name__).addHandler(NullHandler())
|
||||
@@ -1,21 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy._compat
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module makes it easy for wechatpy to run on both Python 2 and 3.
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import sys
|
||||
import six
|
||||
import warnings
|
||||
|
||||
warnings.warn("Module `wechatpy._compat` is deprecated, will be removed in 2.0"
|
||||
"use `wechatpy.utils` instead",
|
||||
DeprecationWarning, stacklevel=2)
|
||||
|
||||
from wechatpy.utils import get_querystring
|
||||
from wechatpy.utils import json
|
||||
@@ -1,140 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
try:
|
||||
from pkgutil import extend_path
|
||||
__path__ = extend_path(__path__, __name__)
|
||||
except ImportError:
|
||||
from pkg_resources import declare_namespace
|
||||
declare_namespace(__name__)
|
||||
|
||||
import time
|
||||
|
||||
from wechatpy.client.base import BaseWeChatClient
|
||||
from wechatpy.client import api
|
||||
|
||||
|
||||
class WeChatClient(BaseWeChatClient):
|
||||
|
||||
"""
|
||||
微信 API 操作类
|
||||
通过这个类可以操作微信 API,发送主动消息、群发消息和创建自定义菜单等。
|
||||
"""
|
||||
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin/'
|
||||
|
||||
menu = api.WeChatMenu()
|
||||
user = api.WeChatUser()
|
||||
group = api.WeChatGroup()
|
||||
media = api.WeChatMedia()
|
||||
card = api.WeChatCard()
|
||||
qrcode = api.WeChatQRCode()
|
||||
message = api.WeChatMessage()
|
||||
misc = api.WeChatMisc()
|
||||
merchant = api.WeChatMerchant()
|
||||
customservice = api.WeChatCustomService()
|
||||
datacube = api.WeChatDataCube()
|
||||
jsapi = api.WeChatJSAPI()
|
||||
material = api.WeChatMaterial()
|
||||
semantic = api.WeChatSemantic()
|
||||
shakearound = api.WeChatShakeAround()
|
||||
device = api.WeChatDevice()
|
||||
template = api.WeChatTemplate()
|
||||
poi = api.WeChatPoi()
|
||||
wifi = api.WeChatWiFi()
|
||||
scan = api.WeChatScan()
|
||||
|
||||
def __init__(self, appid, secret, access_token=None,
|
||||
session=None, timeout=None, auto_retry=True):
|
||||
super(WeChatClient, self).__init__(
|
||||
appid, access_token, session, timeout, auto_retry
|
||||
)
|
||||
self.appid = appid
|
||||
self.secret = secret
|
||||
|
||||
def fetch_access_token(self):
|
||||
"""
|
||||
获取 access token
|
||||
详情请参考 http://mp.weixin.qq.com/wiki/index.php?title=通用接口文档
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._fetch_access_token(
|
||||
url='https://api.weixin.qq.com/cgi-bin/token',
|
||||
params={
|
||||
'grant_type': 'client_credential',
|
||||
'appid': self.appid,
|
||||
'secret': self.secret
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class WeChatComponentClient(WeChatClient):
|
||||
|
||||
"""
|
||||
开放平台代公众号调用客户端
|
||||
"""
|
||||
|
||||
def __init__(self, appid, component, access_token=None,
|
||||
refresh_token=None, session=None, timeout=None):
|
||||
# 未用到secret,所以这里没有
|
||||
super(WeChatComponentClient, self).__init__(
|
||||
appid, '', access_token, session, timeout
|
||||
)
|
||||
self.appid = appid
|
||||
self.component = component
|
||||
# 如果公众号是刚授权,外部还没有缓存access_token和refresh_token
|
||||
# 可以传入这两个值,session 会缓存起来。
|
||||
# 如果外部已经缓存,这里只需要传入 appid,component和session即可
|
||||
if access_token:
|
||||
self.session.set(self.access_token_key, access_token, 7200)
|
||||
if refresh_token:
|
||||
self.session.set(self.refresh_token_key, refresh_token, 7200)
|
||||
|
||||
@property
|
||||
def access_token_key(self):
|
||||
return '{0}_access_token'.format(self.appid)
|
||||
|
||||
@property
|
||||
def refresh_token_key(self):
|
||||
return '{0}_refresh_token'.format(self.appid)
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
access_token = self.session.get(self.access_token_key)
|
||||
if not access_token:
|
||||
self.fetch_access_token()
|
||||
access_token = self.session.get(self.access_token_key)
|
||||
return access_token
|
||||
|
||||
@property
|
||||
def refresh_token(self):
|
||||
return self.session.get(self.refresh_token_key)
|
||||
|
||||
def fetch_access_token(self):
|
||||
"""
|
||||
获取 access token
|
||||
详情请参考 https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list\
|
||||
&t=resource/res_list&verify=1&id=open1419318587&token=&lang=zh_CN
|
||||
|
||||
这是内部刷新机制。请不要完全依赖!
|
||||
因为有可能在缓存期间没有对此公众号的操作,造成refresh_token失效。
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
expires_in = 7200
|
||||
result = self.component.refresh_authorizer_token(
|
||||
self.appid, self.refresh_token)
|
||||
if 'expires_in' in result:
|
||||
expires_in = result['expires_in']
|
||||
self.session.set(
|
||||
self.access_token_key,
|
||||
result['authorizer_access_token'],
|
||||
expires_in
|
||||
)
|
||||
self.session.set(
|
||||
self.refresh_token_key,
|
||||
result['authorizer_refresh_token'],
|
||||
expires_in
|
||||
)
|
||||
self.expires_at = int(time.time()) + expires_in
|
||||
return result
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.client.api.menu import WeChatMenu # NOQA
|
||||
from wechatpy.client.api.user import WeChatUser # NOQA
|
||||
from wechatpy.client.api.card import WeChatCard # NOQA
|
||||
from wechatpy.client.api.group import WeChatGroup # NOQA
|
||||
from wechatpy.client.api.media import WeChatMedia # NOQA
|
||||
from wechatpy.client.api.message import WeChatMessage # NOQA
|
||||
from wechatpy.client.api.qrcode import WeChatQRCode # NOQA
|
||||
from wechatpy.client.api.misc import WeChatMisc # NOQA
|
||||
from wechatpy.client.api.merchant import WeChatMerchant # NOQA
|
||||
from wechatpy.client.api.customservice import WeChatCustomService # NOQA
|
||||
from wechatpy.client.api.datacube import WeChatDataCube # NOQA
|
||||
from wechatpy.client.api.jsapi import WeChatJSAPI # NOQA
|
||||
from wechatpy.client.api.material import WeChatMaterial # NOQA
|
||||
from wechatpy.client.api.semantic import WeChatSemantic # NOQA
|
||||
from wechatpy.client.api.shakearound import WeChatShakeAround # NOQA
|
||||
from wechatpy.client.api.device import WeChatDevice # NOQA
|
||||
from wechatpy.client.api.template import WeChatTemplate # NOQA
|
||||
from wechatpy.client.api.poi import WeChatPoi # NOQA
|
||||
from wechatpy.client.api.wifi import WeChatWiFi # NOQA
|
||||
from wechatpy.client.api.scan import WeChatScan # NOQA
|
||||
@@ -1,29 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
class BaseWeChatAPI(object):
|
||||
|
||||
API_BASE_URL = ''
|
||||
|
||||
""" WeChat API base class """
|
||||
def __init__(self, client=None):
|
||||
self._client = client
|
||||
|
||||
def _get(self, url, **kwargs):
|
||||
if getattr(self, 'API_BASE_URL', None):
|
||||
kwargs['api_base_url'] = self.API_BASE_URL
|
||||
return self._client.get(url, **kwargs)
|
||||
|
||||
def _post(self, url, **kwargs):
|
||||
if getattr(self, 'API_BASE_URL', None):
|
||||
kwargs['api_base_url'] = self.API_BASE_URL
|
||||
return self._client.post(url, **kwargs)
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
return self._client.access_token
|
||||
|
||||
@property
|
||||
def session(self):
|
||||
return self._client.session
|
||||
@@ -1,431 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatCard(BaseWeChatAPI):
|
||||
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/'
|
||||
|
||||
def create(self, card_data):
|
||||
"""
|
||||
创建卡券
|
||||
|
||||
:param card_data: 卡券信息
|
||||
:return: 创建的卡券 ID
|
||||
"""
|
||||
result = self._post(
|
||||
'card/create',
|
||||
data=card_data,
|
||||
result_processor=lambda x: x['card_id']
|
||||
)
|
||||
return result
|
||||
|
||||
def batch_add_locations(self, location_data):
|
||||
"""
|
||||
批量导入门店信息
|
||||
|
||||
:param location_data: 门店信息
|
||||
:return: 门店 ID 列表,插入失败的门店元素值为 -1
|
||||
"""
|
||||
result = self._post(
|
||||
'card/location/batchadd',
|
||||
data=location_data,
|
||||
result_processor=lambda x: x['location_id_list']
|
||||
)
|
||||
return result
|
||||
|
||||
def batch_get_locations(self, offset=0, count=0):
|
||||
"""
|
||||
批量获取门店信息
|
||||
"""
|
||||
return self._post(
|
||||
'card/location/batchget',
|
||||
data={
|
||||
'offset': offset,
|
||||
'count': count
|
||||
}
|
||||
)
|
||||
|
||||
def get_colors(self):
|
||||
"""
|
||||
获得卡券的最新颜色列表,用于创建卡券
|
||||
:return: 颜色列表
|
||||
"""
|
||||
result = self._get(
|
||||
'card/getcolors',
|
||||
result_processor=lambda x: x['colors']
|
||||
)
|
||||
return result
|
||||
|
||||
def create_qrcode(self, qrcode_data):
|
||||
"""
|
||||
创建卡券二维码
|
||||
|
||||
:param qrcode_data: 二维码信息
|
||||
:return: 二维码 ticket,可使用 :func:show_qrcode 换取二维码文件
|
||||
"""
|
||||
result = self._post(
|
||||
'card/qrcode/create',
|
||||
data=qrcode_data,
|
||||
result_processor=lambda x: x['ticket']
|
||||
)
|
||||
return result
|
||||
|
||||
def create_landingpage(self, buffer_data):
|
||||
"""
|
||||
创建货架
|
||||
"""
|
||||
result = self._post(
|
||||
'card/landingpage/create',
|
||||
data=buffer_data
|
||||
)
|
||||
return result
|
||||
|
||||
def get_html(self, card_id):
|
||||
"""
|
||||
图文消息群发卡券
|
||||
"""
|
||||
result = self._post(
|
||||
'card/mpnews/gethtml',
|
||||
data={
|
||||
'card_id': card_id
|
||||
},
|
||||
result_processor=lambda x: x['content']
|
||||
)
|
||||
return result
|
||||
|
||||
def consume_code(self, code, card_id=None):
|
||||
"""
|
||||
消耗 code
|
||||
"""
|
||||
card_data = {
|
||||
'code': code
|
||||
}
|
||||
if card_id:
|
||||
card_data['card_id'] = card_id
|
||||
return self._post(
|
||||
'card/code/consume',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def decrypt_code(self, encrypt_code):
|
||||
"""
|
||||
解码加密的 code
|
||||
"""
|
||||
result = self._post(
|
||||
'card/code/decrypt',
|
||||
data={
|
||||
'encrypt_code': encrypt_code
|
||||
},
|
||||
result_processor=lambda x: x['code']
|
||||
)
|
||||
return result
|
||||
|
||||
def delete(self, card_id):
|
||||
"""
|
||||
删除卡券
|
||||
"""
|
||||
return self._post(
|
||||
'card/delete',
|
||||
data={
|
||||
'card_id': card_id
|
||||
}
|
||||
)
|
||||
|
||||
def get_code(self, code, card_id=None, check_consume=True):
|
||||
"""
|
||||
查询 code 信息
|
||||
"""
|
||||
card_data = {
|
||||
'code': code
|
||||
}
|
||||
if card_id:
|
||||
card_data['card_id'] = card_id
|
||||
if not check_consume:
|
||||
card_data['check_consume'] = check_consume
|
||||
return self._post(
|
||||
'card/code/get',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def get_card_list(self, openid, card_id=None):
|
||||
"""
|
||||
用于获取用户卡包里的,属于该appid下的卡券。
|
||||
"""
|
||||
card_data = {
|
||||
'openid': openid
|
||||
}
|
||||
if card_id:
|
||||
card_data['card_id'] = card_id
|
||||
return self._post(
|
||||
'card/user/getcardlist',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def batch_get(self, offset=0, count=50, status_list=None):
|
||||
"""
|
||||
批量查询卡券信息
|
||||
"""
|
||||
card_data = {
|
||||
'offset': offset,
|
||||
'count': count
|
||||
}
|
||||
if status_list:
|
||||
card_data['status_list'] = status_list
|
||||
return self._post(
|
||||
'card/batchget',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def get(self, card_id):
|
||||
"""
|
||||
查询卡券详情
|
||||
"""
|
||||
result = self._post(
|
||||
'card/get',
|
||||
data={
|
||||
'card_id': card_id
|
||||
},
|
||||
result_processor=lambda x: x['card']
|
||||
)
|
||||
return result
|
||||
|
||||
def update_code(self, card_id, old_code, new_code):
|
||||
"""
|
||||
更新卡券 code
|
||||
"""
|
||||
return self._post(
|
||||
'card/code/update',
|
||||
data={
|
||||
'card_id': card_id,
|
||||
'code': old_code,
|
||||
'new_code': new_code
|
||||
}
|
||||
)
|
||||
|
||||
def invalid_code(self, code, card_id=None):
|
||||
"""
|
||||
设置卡券失效
|
||||
"""
|
||||
card_data = {
|
||||
'code': code
|
||||
}
|
||||
if card_id:
|
||||
card_data['card_id'] = card_id
|
||||
return self._post(
|
||||
'card/code/unavailable',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def update(self, card_data):
|
||||
"""
|
||||
更新卡券信息
|
||||
"""
|
||||
return self._post(
|
||||
'card/update',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def set_paycell(self, card_id, is_open):
|
||||
"""
|
||||
更新卡券信息
|
||||
"""
|
||||
return self._post(
|
||||
'card/paycell/set',
|
||||
data={
|
||||
'card_id': card_id,
|
||||
'is_open': is_open
|
||||
}
|
||||
)
|
||||
|
||||
def set_test_whitelist(self, openids=None, usernames=None):
|
||||
"""
|
||||
设置卡券测试用户白名单
|
||||
"""
|
||||
openids = openids or []
|
||||
usernames = usernames or []
|
||||
return self._post(
|
||||
'card/testwhitelist/set',
|
||||
data={
|
||||
'openid': openids,
|
||||
'username': usernames
|
||||
}
|
||||
)
|
||||
|
||||
def activate_membercard(self, membership_number, code, init_bonus=0,
|
||||
init_balance=0, card_id=None):
|
||||
"""
|
||||
激活/绑定会员卡
|
||||
"""
|
||||
card_data = {
|
||||
'membership_number': membership_number,
|
||||
'code': code,
|
||||
'init_bonus': init_bonus,
|
||||
'init_balance': init_balance
|
||||
}
|
||||
if card_id:
|
||||
card_data['card_id'] = card_id
|
||||
return self._post(
|
||||
'card/membercard/activate',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def update_membercard(self, code, add_bonus=0, record_bonus='',
|
||||
add_balance=0, record_balance='', card_id=None):
|
||||
"""
|
||||
会员卡交易更新信息
|
||||
"""
|
||||
card_data = {
|
||||
'code': code,
|
||||
'add_bonus': add_bonus,
|
||||
'add_balance': add_balance,
|
||||
'record_bonus': record_bonus,
|
||||
'record_balance': record_balance
|
||||
}
|
||||
if card_id:
|
||||
card_data['card_id'] = card_id
|
||||
return self._post(
|
||||
'card/membercard/updateuser',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def update_movie_ticket(self, code, ticket_class, show_time, duration,
|
||||
screening_room, seat_number, card_id=None):
|
||||
"""
|
||||
更新电影票
|
||||
"""
|
||||
ticket = {
|
||||
'code': code,
|
||||
'ticket_class': ticket_class,
|
||||
'show_time': show_time,
|
||||
'duration': duration,
|
||||
'screening_room': screening_room,
|
||||
'seat_number': seat_number
|
||||
}
|
||||
if card_id:
|
||||
ticket['card_id'] = card_id
|
||||
return self._post(
|
||||
'card/movieticket/updateuser',
|
||||
data=ticket
|
||||
)
|
||||
|
||||
def checkin_boardingpass(self, code, passenger_name, seat_class,
|
||||
etkt_bnr, seat='', gate='', boarding_time=None,
|
||||
is_cancel=False, qrcode_data=None, card_id=None):
|
||||
"""
|
||||
飞机票接口
|
||||
"""
|
||||
data = {
|
||||
'code': code,
|
||||
'passenger_name': passenger_name,
|
||||
'class': seat_class,
|
||||
'etkt_bnr': etkt_bnr,
|
||||
'seat': seat,
|
||||
'gate': gate,
|
||||
'is_cancel': is_cancel
|
||||
}
|
||||
if boarding_time:
|
||||
data['boarding_time'] = boarding_time
|
||||
if qrcode_data:
|
||||
data['qrcode_data'] = qrcode_data
|
||||
if card_id:
|
||||
data['card_id'] = card_id
|
||||
return self._post(
|
||||
'card/boardingpass/checkin',
|
||||
data=data
|
||||
)
|
||||
|
||||
def update_luckymoney_balance(self, code, balance, card_id=None):
|
||||
"""
|
||||
更新红包余额
|
||||
"""
|
||||
card_data = {
|
||||
'code': code,
|
||||
'balance': balance
|
||||
}
|
||||
if card_id:
|
||||
card_data['card_id'] = card_id
|
||||
return self._post(
|
||||
'card/luckymoney/updateuserbalance',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def get_redirect_url(self, url, encrypt_code, card_id):
|
||||
"""
|
||||
获取卡券跳转外链
|
||||
"""
|
||||
from wechatpy.utils import WeChatSigner
|
||||
|
||||
code = self.decrypt_code(encrypt_code)
|
||||
|
||||
signer = WeChatSigner()
|
||||
signer.add_data(self.secret)
|
||||
signer.add_data(code)
|
||||
signer.add_data(card_id)
|
||||
signature = signer.signature
|
||||
|
||||
r = '{url}?encrypt_code={code}&card_id={card_id}&signature={signature}'
|
||||
return r.format(
|
||||
url=url,
|
||||
code=encrypt_code,
|
||||
card_id=card_id,
|
||||
signature=signature
|
||||
)
|
||||
|
||||
def deposit_code(self, card_id, codes):
|
||||
"""
|
||||
导入code
|
||||
"""
|
||||
card_data = {
|
||||
'card_id': card_id,
|
||||
'code': codes
|
||||
}
|
||||
return self._post(
|
||||
'card/code/deposit',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def get_deposit_count(self, card_id):
|
||||
"""
|
||||
查询导入code数目
|
||||
"""
|
||||
card_data = {
|
||||
'card_id': card_id,
|
||||
}
|
||||
return self._post(
|
||||
'card/code/getdepositcount',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def check_code(self, card_id, codes):
|
||||
"""
|
||||
核查code
|
||||
"""
|
||||
card_data = {
|
||||
'card_id': card_id,
|
||||
'code': codes
|
||||
}
|
||||
return self._post(
|
||||
'card/code/checkcode',
|
||||
data=card_data
|
||||
)
|
||||
|
||||
def modify_stock(self, card_id, n):
|
||||
"""
|
||||
修改库存
|
||||
"""
|
||||
if n == 0:
|
||||
return
|
||||
card_data = {
|
||||
'card_id': card_id,
|
||||
}
|
||||
if n > 0:
|
||||
card_data['increase_stock_value'] = n
|
||||
elif n < 0:
|
||||
card_data['reduce_stock_value'] = -n
|
||||
return self._post(
|
||||
'card/modifystock',
|
||||
data=card_data
|
||||
)
|
||||
@@ -1,242 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import hashlib
|
||||
import time
|
||||
import datetime
|
||||
|
||||
from six.moves.urllib.parse import quote
|
||||
from optionaldict import optionaldict
|
||||
from wechatpy.utils import to_binary
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatCustomService(BaseWeChatAPI):
|
||||
|
||||
def add_account(self, account, nickname, password):
|
||||
"""
|
||||
添加客服账号
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
|
||||
|
||||
:param account: 完整客服账号,格式为:账号前缀@公众号微信号
|
||||
:param nickname: 客服昵称,最长6个汉字或12个英文字符
|
||||
:param password: 客服账号登录密码
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
password = to_binary(password)
|
||||
password = hashlib.md5(password).hexdigest()
|
||||
return self._post(
|
||||
'https://api.weixin.qq.com/customservice/kfaccount/add',
|
||||
data={
|
||||
'kf_account': account,
|
||||
'nickname': nickname,
|
||||
'password': password
|
||||
}
|
||||
)
|
||||
|
||||
def update_account(self, account, nickname, password):
|
||||
"""
|
||||
更新客服账号
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
|
||||
|
||||
:param account: 完整客服账号,格式为:账号前缀@公众号微信号
|
||||
:param nickname: 客服昵称,最长6个汉字或12个英文字符
|
||||
:param password: 客服账号登录密码
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
password = to_binary(password)
|
||||
password = hashlib.md5(password).hexdigest()
|
||||
return self._post(
|
||||
'https://api.weixin.qq.com/customservice/kfaccount/update',
|
||||
data={
|
||||
'kf_account': account,
|
||||
'nickname': nickname,
|
||||
'password': password
|
||||
}
|
||||
)
|
||||
|
||||
def delete_account(self, account):
|
||||
"""
|
||||
删除客服账号
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
|
||||
|
||||
:param account: 完整客服账号,格式为:账号前缀@公众号微信号
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
params_data = [
|
||||
'access_token={0}'.format(quote(self.access_token)),
|
||||
'kf_account={0}'.format(quote(to_binary(account), safe=b'/@')),
|
||||
]
|
||||
params = '&'.join(params_data)
|
||||
return self._get(
|
||||
'https://api.weixin.qq.com/customservice/kfaccount/del',
|
||||
params=params
|
||||
)
|
||||
|
||||
def get_accounts(self):
|
||||
"""
|
||||
获取客服账号列表
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
|
||||
|
||||
:return: 客服账号列表
|
||||
"""
|
||||
res = self._get(
|
||||
'customservice/getkflist',
|
||||
result_processor=lambda x: x['kf_list']
|
||||
)
|
||||
return res
|
||||
|
||||
def upload_headimg(self, account, media_file):
|
||||
"""
|
||||
上传客服账号头像
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
|
||||
|
||||
:param account: 完整客服账号
|
||||
:param media_file: 要上传的头像文件,一个 File-Object
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'https://api.weixin.qq.com/customservice/kfaccount/uploadheadimg',
|
||||
params={
|
||||
'kf_account': account
|
||||
},
|
||||
files={
|
||||
'media': media_file
|
||||
}
|
||||
)
|
||||
|
||||
def get_online_accounts(self):
|
||||
"""
|
||||
获取在线客服接待信息
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/9/6fff6f191ef92c126b043ada035cc935.html
|
||||
|
||||
:return: 客服接待信息列表
|
||||
"""
|
||||
res = self._get(
|
||||
'customservice/getonlinekflist',
|
||||
result_processor=lambda x: x['kf_online_list']
|
||||
)
|
||||
return res
|
||||
|
||||
def create_session(self, openid, account, text=None):
|
||||
"""
|
||||
多客服创建会话
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
|
||||
|
||||
:param openid: 客户 openid
|
||||
:param account: 完整客服账号
|
||||
:param text: 附加信息,可选
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict(
|
||||
openid=openid,
|
||||
kf_account=account,
|
||||
text=text
|
||||
)
|
||||
return self._post(
|
||||
'https://api.weixin.qq.com/customservice/kfsession/create',
|
||||
data=data
|
||||
)
|
||||
|
||||
def close_session(self, openid, account, text=None):
|
||||
"""
|
||||
多客服关闭会话
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
|
||||
|
||||
:param openid: 客户 openid
|
||||
:param account: 完整客服账号
|
||||
:param text: 附加信息,可选
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict(
|
||||
openid=openid,
|
||||
kf_account=account,
|
||||
text=text
|
||||
)
|
||||
return self._post(
|
||||
'https://api.weixin.qq.com/customservice/kfsession/close',
|
||||
data=data
|
||||
)
|
||||
|
||||
def get_session(self, openid):
|
||||
"""
|
||||
获取客户的会话状态
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
|
||||
|
||||
:param openid: 客户 openid
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'https://api.weixin.qq.com/customservice/kfsession/getsession',
|
||||
params={'openid': openid}
|
||||
)
|
||||
|
||||
def get_session_list(self, account):
|
||||
"""
|
||||
获取客服的会话列表
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
|
||||
|
||||
:param account: 完整客服账号
|
||||
:return: 客服的会话列表
|
||||
"""
|
||||
res = self._get(
|
||||
'https://api.weixin.qq.com/customservice/kfsession/getsessionlist',
|
||||
params={'kf_account': account},
|
||||
result_processor=lambda x: x['sessionlist']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_wait_case(self):
|
||||
"""
|
||||
获取未接入会话列表
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/2/6c20f3e323bdf5986cfcb33cbd3b829a.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'https://api.weixin.qq.com/customservice/kfsession/getwaitcase'
|
||||
)
|
||||
|
||||
def get_records(self, start_time, end_time, page_index,
|
||||
page_size=10, user_id=None):
|
||||
"""
|
||||
获取客服聊天记录
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/19/7c129ec71ddfa60923ea9334557e8b23.html
|
||||
|
||||
:param start_time: 查询开始时间,UNIX 时间戳
|
||||
:param end_time: 查询结束时间,UNIX 时间戳,每次查询不能跨日查询
|
||||
:param page_index: 查询第几页,从 1 开始
|
||||
:param page_size: 每页大小,每页最多拉取 1000 条
|
||||
:param user_id: 普通用户的标识,对当前公众号唯一
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
if isinstance(start_time, datetime.datetime):
|
||||
start_time = time.mktime(start_time.timetuple())
|
||||
if isinstance(end_time, datetime.datetime):
|
||||
end_time = time.mktime(end_time.timetuple())
|
||||
record_data = {
|
||||
'starttime': int(start_time),
|
||||
'endtime': int(end_time),
|
||||
'pageindex': page_index,
|
||||
'pagesize': page_size
|
||||
}
|
||||
if user_id:
|
||||
record_data['openid'] = user_id
|
||||
res = self._post(
|
||||
'https://api.weixin.qq.com/customservice/msgrecord/getrecord',
|
||||
data=record_data,
|
||||
result_processor=lambda x: x['recordlist']
|
||||
)
|
||||
return res
|
||||
@@ -1,360 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import datetime
|
||||
|
||||
import six
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatDataCube(BaseWeChatAPI):
|
||||
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/datacube/'
|
||||
|
||||
@classmethod
|
||||
def _to_date_str(cls, date):
|
||||
if isinstance(date, (datetime.datetime, datetime.date)):
|
||||
return date.strftime('%Y-%m-%d')
|
||||
elif isinstance(date, six.string_types):
|
||||
return date
|
||||
else:
|
||||
raise ValueError('Can not convert %s type to str', type(date))
|
||||
|
||||
def get_user_summary(self, begin_date, end_date):
|
||||
"""
|
||||
获取用户增减数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/3/ecfed6e1a0a03b5f35e5efac98e864b7.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getusersummary',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
}
|
||||
)
|
||||
return res['list']
|
||||
|
||||
def get_user_cumulate(self, begin_date, end_date):
|
||||
"""
|
||||
获取累计用户数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/3/ecfed6e1a0a03b5f35e5efac98e864b7.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getusercumulate',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_interface_summary(self, begin_date, end_date):
|
||||
"""
|
||||
获取接口分析数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/30ed81ae38cf4f977194bf1a5db73668.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getinterfacesummary',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_interface_summary_hour(self, begin_date, end_date):
|
||||
"""
|
||||
获取接口分析分时数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/30ed81ae38cf4f977194bf1a5db73668.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getinterfacesummaryhour',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_article_summary(self, begin_date, end_date):
|
||||
"""
|
||||
获取图文群发每日数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getarticlesummary',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_article_total(self, begin_date, end_date):
|
||||
"""
|
||||
获取图文群发总数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getarticletotal',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_user_read(self, begin_date, end_date):
|
||||
"""
|
||||
获取图文统计数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getuserread',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_user_read_hour(self, begin_date, end_date):
|
||||
"""
|
||||
获取图文分时统计数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getuserreadhour',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_user_share(self, begin_date, end_date):
|
||||
"""
|
||||
获取图文分享转发数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getusershare',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_user_share_hour(self, begin_date, end_date):
|
||||
"""
|
||||
获取图文分享转发分时数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/c0453610fb5131d1fcb17b4e87c82050.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getusersharehour',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_upstream_msg(self, begin_date, end_date):
|
||||
"""
|
||||
获取消息发送概况数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getupstreammsg',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_upstream_msg_hour(self, begin_date, end_date):
|
||||
"""
|
||||
获取消息发送分时数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getupstreammsghour',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_upstream_msg_week(self, begin_date, end_date):
|
||||
"""
|
||||
获取消息发送周数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getupstreammsgweek',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_upstream_msg_month(self, begin_date, end_date):
|
||||
"""
|
||||
获取消息发送月数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getupstreammsgmonth',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_upstream_msg_dist(self, begin_date, end_date):
|
||||
"""
|
||||
获取消息发送分布数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getupstreammsgdist',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_upstream_msg_dist_week(self, begin_date, end_date):
|
||||
"""
|
||||
获取消息发送分布数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getupstreammsgdistweek',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_upstream_msg_dist_month(self, begin_date, end_date):
|
||||
"""
|
||||
获取消息发送分布数据
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/32d42ad542f2e4fc8a8aa60e1bce9838.html
|
||||
|
||||
:param begin_date: 起始日期
|
||||
:param end_date: 结束日期
|
||||
:return: 统计数据列表
|
||||
"""
|
||||
res = self._post(
|
||||
'getupstreammsgdistmonth',
|
||||
data={
|
||||
'begin_date': self._to_date_str(begin_date),
|
||||
'end_date': self._to_date_str(end_date)
|
||||
},
|
||||
result_processor=lambda x: x['list']
|
||||
)
|
||||
return res
|
||||
@@ -1,284 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import base64
|
||||
import urllib
|
||||
|
||||
from wechatpy.utils import to_text, to_binary
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatDevice(BaseWeChatAPI):
|
||||
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/device/'
|
||||
|
||||
def send_message(self, device_type, device_id, user_id, content):
|
||||
"""
|
||||
主动发送消息给设备
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_3.html
|
||||
|
||||
:param device_type: 设备类型,目前为“公众账号原始ID”
|
||||
:param device_id: 设备ID
|
||||
:param user_id: 微信用户账号的openid
|
||||
:param content: 消息内容,BASE64编码
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
content = to_text(base64.b64encode(to_binary(content)))
|
||||
return self._post(
|
||||
'transmsg',
|
||||
data={
|
||||
'device_type': device_type,
|
||||
'device_id': device_id,
|
||||
'openid': user_id,
|
||||
'content': content
|
||||
}
|
||||
)
|
||||
|
||||
def create_qrcode(self, device_ids):
|
||||
"""
|
||||
获取设备二维码
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_5.html
|
||||
|
||||
:param device_ids: 设备id的列表
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'create_qrcode',
|
||||
data={
|
||||
'device_num': len(device_ids),
|
||||
'device_id_list': device_ids
|
||||
}
|
||||
)
|
||||
|
||||
def get_qrcode_url(self, ticket, data=None):
|
||||
"""
|
||||
通过 ticket 换取二维码地址
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_5.html
|
||||
|
||||
:param ticket: 二维码 ticket
|
||||
:param data: 额外数据
|
||||
:return: 二维码地址
|
||||
"""
|
||||
url = 'http://we.qq.com/d/{ticket}'.format(ticket=ticket)
|
||||
if data:
|
||||
if isinstance(data, (dict, tuple, list)):
|
||||
data = urllib.urlencode(data)
|
||||
data = to_text(base64.b64encode(to_binary(data)))
|
||||
url = '{base}#{data}'.format(base=url, data=data)
|
||||
return url
|
||||
|
||||
def bind(self, ticket, device_id, user_id):
|
||||
"""
|
||||
绑定设备
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_12.html
|
||||
|
||||
:param ticket: 绑定操作合法性的凭证(由微信后台生成,第三方H5通过客户端jsapi获得)
|
||||
:param device_id: 设备id
|
||||
:param user_id: 用户对应的openid
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'bind',
|
||||
data={
|
||||
'ticket': ticket,
|
||||
'device_id': device_id,
|
||||
'openid': user_id
|
||||
}
|
||||
)
|
||||
|
||||
def unbind(self, ticket, device_id, user_id):
|
||||
"""
|
||||
解绑设备
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_12.html
|
||||
|
||||
:param ticket: 绑定操作合法性的凭证(由微信后台生成,第三方H5通过客户端jsapi获得)
|
||||
:param device_id: 设备id
|
||||
:param user_id: 用户对应的openid
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'unbind',
|
||||
data={
|
||||
'ticket': ticket,
|
||||
'device_id': device_id,
|
||||
'openid': user_id
|
||||
}
|
||||
)
|
||||
|
||||
def compel_bind(self, device_id, user_id):
|
||||
"""
|
||||
强制绑定用户和设备
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_12.html
|
||||
|
||||
:param device_id: 设备id
|
||||
:param user_id: 用户对应的openid
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'compel_bind',
|
||||
data={
|
||||
'device_id': device_id,
|
||||
'openid': user_id
|
||||
}
|
||||
)
|
||||
|
||||
force_bind = compel_bind
|
||||
|
||||
def compel_unbind(self, device_id, user_id):
|
||||
"""
|
||||
强制解绑用户和设备
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_12.html
|
||||
|
||||
:param device_id: 设备id
|
||||
:param user_id: 用户对应的openid
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'compel_unbind',
|
||||
data={
|
||||
'device_id': device_id,
|
||||
'openid': user_id
|
||||
}
|
||||
)
|
||||
|
||||
force_unbind = compel_unbind
|
||||
|
||||
def get_stat(self, device_id):
|
||||
"""
|
||||
设备状态查询
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_7.html
|
||||
|
||||
:param device_id: 设备id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'get_stat',
|
||||
data={'device_id': device_id}
|
||||
)
|
||||
|
||||
def verify_qrcode(self, ticket):
|
||||
"""
|
||||
验证二维码
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_9.html
|
||||
|
||||
:param ticket: 设备二维码的ticket
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'verify_qrcode',
|
||||
data={'ticket': ticket}
|
||||
)
|
||||
|
||||
def get_user_id(self, device_type, device_id):
|
||||
"""
|
||||
获取设备绑定openID
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_4.html
|
||||
|
||||
:param device_type: 设备类型,目前为“公众账号原始ID”
|
||||
:param device_id: 设备id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'get_openid',
|
||||
data={
|
||||
'device_type': device_type,
|
||||
'device_id': device_id
|
||||
}
|
||||
)
|
||||
|
||||
get_open_id = get_user_id
|
||||
|
||||
def get_binded_devices(self, user_id):
|
||||
"""
|
||||
通过openid获取用户在当前devicetype下绑定的deviceid列表
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_13.html
|
||||
|
||||
:param user_id: 要查询的用户的openid
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'get_bind_device',
|
||||
data={'openid': user_id}
|
||||
)
|
||||
|
||||
get_bind_device = get_binded_devices
|
||||
|
||||
def send_status_message(self, device_type, device_id, user_id, status):
|
||||
"""
|
||||
主动发送设备状态消息给微信终端
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_10.html
|
||||
|
||||
:param device_type: 设备类型,目前为“公众账号原始ID”
|
||||
:param device_id: 设备ID
|
||||
:param user_id: 微信用户账号的openid
|
||||
:param status: 设备状态:0--未连接, 1--已连接
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'transmsg',
|
||||
data={
|
||||
'device_type': device_type,
|
||||
'device_id': device_id,
|
||||
'open_id': user_id,
|
||||
'device_status': status
|
||||
}
|
||||
)
|
||||
|
||||
def authorize(self, devices, op_type=0):
|
||||
"""
|
||||
设备授权
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_6.html
|
||||
|
||||
:param devices: 设备信息的列表
|
||||
:param op_type: 请求操作的类型,限定取值为:0:设备授权 1:设备更新
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'authorize',
|
||||
data={
|
||||
'device_num': len(devices),
|
||||
'device_list': devices,
|
||||
'op_type': op_type
|
||||
}
|
||||
)
|
||||
|
||||
def get_qrcode(self):
|
||||
"""
|
||||
获取deviceid和二维码
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_11.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get('getqrcode')
|
||||
|
||||
def authorize_device(self, devices, op_type=1):
|
||||
"""
|
||||
设备授权
|
||||
详情请参考
|
||||
http://iot.weixin.qq.com/document-2_6.html
|
||||
|
||||
:param devices: 设备信息的列表
|
||||
:param op_type: 请求操作的类型,限定取值为:0:设备授权 1:设备更新
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'authorize_device',
|
||||
data={
|
||||
'device_num': len(devices),
|
||||
'device_list': devices,
|
||||
'op_type': op_type
|
||||
}
|
||||
)
|
||||
@@ -1,148 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.utils import to_text
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatGroup(BaseWeChatAPI):
|
||||
|
||||
def create(self, name):
|
||||
"""
|
||||
创建分组
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
|
||||
|
||||
:param name: 分组名字(30个字符以内)
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.group.create('New Group')
|
||||
|
||||
"""
|
||||
name = to_text(name)
|
||||
return self._post(
|
||||
'groups/create',
|
||||
data={'group': {'name': name}}
|
||||
)
|
||||
|
||||
def get(self, user_id=None):
|
||||
"""
|
||||
查询所有分组或查询用户所在分组 ID
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
|
||||
|
||||
:param user_id: 用户 ID,提供时查询该用户所在分组,否则查询所有分组
|
||||
:return: 所有分组列表或用户所在分组 ID
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
group = client.group.get('openid')
|
||||
|
||||
"""
|
||||
if user_id is None:
|
||||
res = self._get(
|
||||
'groups/get',
|
||||
result_processor=lambda x: x['groups']
|
||||
)
|
||||
else:
|
||||
res = self._post(
|
||||
'groups/getid',
|
||||
data={'openid': user_id},
|
||||
result_processor=lambda x: x['groupid']
|
||||
)
|
||||
return res
|
||||
|
||||
def update(self, group_id, name):
|
||||
"""
|
||||
修改分组名
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
|
||||
|
||||
:param group_id: 分组id,由微信分配
|
||||
:param name: 分组名字(30个字符以内)
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.group.update(1234, 'New Name')
|
||||
|
||||
"""
|
||||
name = to_text(name)
|
||||
return self._post(
|
||||
'groups/update',
|
||||
data={
|
||||
'group': {
|
||||
'id': int(group_id),
|
||||
'name': name
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def move_user(self, user_id, group_id):
|
||||
"""
|
||||
移动用户分组
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
|
||||
|
||||
:param user_id: 用户 ID, 可以是单个或者列表,为列表时为批量移动用户分组
|
||||
:param group_id: 分组 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.group.move_user('openid', 1234)
|
||||
|
||||
"""
|
||||
data = {'to_groupid': group_id}
|
||||
if isinstance(user_id, (tuple, list)):
|
||||
endpoint = 'groups/members/batchupdate'
|
||||
data['openid_list'] = user_id
|
||||
else:
|
||||
endpoint = 'groups/members/update'
|
||||
data['openid'] = user_id
|
||||
return self._post(endpoint, data=data)
|
||||
|
||||
def delete(self, group_id):
|
||||
"""
|
||||
删除分组
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
|
||||
|
||||
:param group_id: 分组 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.group.delete(1234)
|
||||
|
||||
"""
|
||||
return self._post(
|
||||
'groups/delete',
|
||||
data={
|
||||
'group': {
|
||||
'id': group_id
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -1,58 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.client.jsapi
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides some APIs for JS SDK
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
|
||||
from wechatpy.utils import WeChatSigner
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatJSAPI(BaseWeChatAPI):
|
||||
|
||||
def get_ticket(self, type='jsapi'):
|
||||
"""
|
||||
获取微信 JS-SDK ticket
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'ticket/getticket',
|
||||
params={'type': type}
|
||||
)
|
||||
|
||||
def get_jsapi_ticket(self):
|
||||
"""
|
||||
获取微信 JS-SDK ticket
|
||||
|
||||
该方法会通过 session 对象自动缓存管理 ticket
|
||||
|
||||
:return: ticket
|
||||
"""
|
||||
ticket = self.session.get('jsapi_ticket')
|
||||
expires_at = self.session.get('jsapi_ticket_expires_at', 0)
|
||||
if not ticket or expires_at < int(time.time()):
|
||||
jsapi_ticket = self.get_ticket('jsapi')
|
||||
ticket = jsapi_ticket['ticket']
|
||||
expires_at = int(time.time()) + int(jsapi_ticket['expires_in'])
|
||||
self.session.set('jsapi_ticket', ticket)
|
||||
self.session.set('jsapi_ticket_expires_at', expires_at)
|
||||
return ticket
|
||||
|
||||
def get_jsapi_signature(self, noncestr, ticket, timestamp, url):
|
||||
data = [
|
||||
'noncestr={noncestr}'.format(noncestr=noncestr),
|
||||
'jsapi_ticket={ticket}'.format(ticket=ticket),
|
||||
'timestamp={timestamp}'.format(timestamp=timestamp),
|
||||
'url={url}'.format(url=url),
|
||||
]
|
||||
signer = WeChatSigner(delimiter=b'&')
|
||||
signer.add_data(*data)
|
||||
return signer.signature
|
||||
@@ -1,167 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.utils import json
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMaterial(BaseWeChatAPI):
|
||||
|
||||
def add_articles(self, articles):
|
||||
"""
|
||||
新增永久图文素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/14/7e6c03263063f4813141c3e17dd4350a.html
|
||||
|
||||
:param articles: 图文素材数组
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
articles_data = []
|
||||
for article in articles:
|
||||
articles_data.append({
|
||||
'thumb_media_id': article['thumb_media_id'],
|
||||
'title': article['title'],
|
||||
'content': article['content'],
|
||||
'author': article.get('author', ''),
|
||||
'content_source_url': article.get('content_source_url', ''),
|
||||
'digest': article.get('digest', ''),
|
||||
'show_cover_pic': article.get('show_cover_pic', '0')
|
||||
})
|
||||
return self._post(
|
||||
'material/add_news',
|
||||
data={
|
||||
'articles': articles_data
|
||||
}
|
||||
)
|
||||
|
||||
def add(self, media_type, media_file, title=None, introduction=None):
|
||||
"""
|
||||
新增其它类型永久素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/14/7e6c03263063f4813141c3e17dd4350a.html
|
||||
|
||||
:param media_type: 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
|
||||
:param media_file: 要上传的文件,一个 File-object
|
||||
:param title: 视频素材标题,仅上传视频素材时需要
|
||||
:param introduction: 视频素材简介,仅上传视频素材时需要
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
params = {
|
||||
'access_token': self.access_token,
|
||||
'type': media_type
|
||||
}
|
||||
if media_type == 'video':
|
||||
assert title, 'Video title must be set'
|
||||
assert introduction, 'Video introduction must be set'
|
||||
description = {
|
||||
'title': title,
|
||||
'introduction': introduction
|
||||
}
|
||||
params['description'] = json.dumps(description)
|
||||
return self._post(
|
||||
'material/add_material',
|
||||
params=params,
|
||||
files={
|
||||
'media': media_file
|
||||
}
|
||||
)
|
||||
|
||||
def get(self, media_id):
|
||||
"""
|
||||
获取永久素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/4/b3546879f07623cb30df9ca0e420a5d0.html
|
||||
|
||||
:param media_id: 素材的 media_id
|
||||
:return: 图文素材返回图文列表,其它类型为素材的内容
|
||||
"""
|
||||
def _processor(res):
|
||||
if isinstance(res, dict):
|
||||
# 图文素材
|
||||
return res.get('news_item', [])
|
||||
return res
|
||||
|
||||
res = self._post(
|
||||
'material/get_material',
|
||||
data={
|
||||
'media_id': media_id
|
||||
},
|
||||
result_processor=_processor
|
||||
)
|
||||
return res
|
||||
|
||||
def delete(self, media_id):
|
||||
"""
|
||||
删除永久素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/5/e66f61c303db51a6c0f90f46b15af5f5.html
|
||||
|
||||
:param media_id: 素材的 media_id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'material/del_material',
|
||||
data={
|
||||
'media_id': media_id
|
||||
}
|
||||
)
|
||||
|
||||
def update_articles(self, media_id, index, articles):
|
||||
"""
|
||||
修改永久图文素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/4/19a59cba020d506e767360ca1be29450.html
|
||||
|
||||
:param media_id: 要修改的图文消息的 id
|
||||
:param index: 要更新的文章在图文消息中的位置(多图文消息时,此字段才有意义),第一篇为 0
|
||||
:param articles: 图文素材数组
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
articles_data = []
|
||||
for article in articles:
|
||||
articles_data.append({
|
||||
'thumb_media_id': article['thumb_media_id'],
|
||||
'title': article['title'],
|
||||
'content': article['content'],
|
||||
'author': article.get('author', ''),
|
||||
'content_source_url': article.get('content_source_url', ''),
|
||||
'digest': article.get('digest', ''),
|
||||
'show_cover_pic': article.get('show_cover_pic', '0')
|
||||
})
|
||||
return self._post(
|
||||
'material/update_news',
|
||||
data={
|
||||
'media_id': media_id,
|
||||
'index': index,
|
||||
'articles': articles_data
|
||||
}
|
||||
)
|
||||
|
||||
def batchget(self, media_type, offset=0, count=20):
|
||||
"""
|
||||
批量获取永久素材列表
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/2108cd7aafff7f388f41f37efa710204.html
|
||||
|
||||
:param media_type: 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(news)
|
||||
:param offset: 从全部素材的该偏移位置开始返回,0 表示从第一个素材返回
|
||||
:param count: 返回素材的数量,取值在1到20之间
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'material/batchget_material',
|
||||
data={
|
||||
'type': media_type,
|
||||
'offset': offset,
|
||||
'count': count
|
||||
}
|
||||
)
|
||||
|
||||
def get_count(self):
|
||||
"""
|
||||
获取素材总数
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/8cc64f8c189674b421bee3ed403993b8.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get('material/get_materialcount')
|
||||
@@ -1,126 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMedia(BaseWeChatAPI):
|
||||
|
||||
def upload(self, media_type, media_file):
|
||||
"""
|
||||
上传临时素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/5/963fc70b80dc75483a271298a76a8d59.html
|
||||
|
||||
:param media_type: 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和缩略图(thumb)
|
||||
:param media_file: 要上传的文件,一个 File-object
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
url='http://file.api.weixin.qq.com/cgi-bin/media/upload',
|
||||
params={
|
||||
'type': media_type
|
||||
},
|
||||
files={
|
||||
'media': media_file
|
||||
}
|
||||
)
|
||||
|
||||
def download(self, media_id):
|
||||
"""
|
||||
获取临时素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/10/78b15308b053286e2a66b33f0f0f5fb6.html
|
||||
|
||||
:param media_id: 媒体文件 ID
|
||||
|
||||
:return: requests 的 Response 实例
|
||||
"""
|
||||
return self._get(
|
||||
'http://file.api.weixin.qq.com/cgi-bin/media/get',
|
||||
params={
|
||||
'media_id': media_id
|
||||
}
|
||||
)
|
||||
|
||||
def get_url(self, media_id):
|
||||
"""
|
||||
获取临时素材下载地址
|
||||
|
||||
:param media_id: 媒体文件 ID
|
||||
:return: 临时素材下载地址
|
||||
"""
|
||||
parts = (
|
||||
'http://file.api.weixin.qq.com/cgi-bin/media/get',
|
||||
'?access_token=',
|
||||
self.access_token,
|
||||
'&media_id=',
|
||||
media_id
|
||||
)
|
||||
return ''.join(parts)
|
||||
|
||||
def upload_video(self, media_id, title, description):
|
||||
"""
|
||||
群发视频消息时获取视频 media_id
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param media_id: 需通过基础支持中的上传下载多媒体文件 :func:`upload` 来得到
|
||||
:param title: 视频标题
|
||||
:param description: 视频描述
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
url='https://file.api.weixin.qq.com/cgi-bin/media/uploadvideo',
|
||||
data={
|
||||
'media_id': media_id,
|
||||
'title': title,
|
||||
'description': description
|
||||
}
|
||||
)
|
||||
|
||||
def upload_articles(self, articles):
|
||||
"""
|
||||
上传图文消息素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param articles: 图文消息数组
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
articles_data = []
|
||||
for article in articles:
|
||||
articles_data.append({
|
||||
'thumb_media_id': article['thumb_media_id'],
|
||||
'title': article['title'],
|
||||
'content': article['content'],
|
||||
'author': article.get('author', ''),
|
||||
'content_source_url': article.get('content_source_url', ''),
|
||||
'digest': article.get('digest', ''),
|
||||
'show_cover_pic': article.get('show_cover_pic', '0')
|
||||
})
|
||||
return self._post(
|
||||
'media/uploadnews',
|
||||
data={
|
||||
'articles': articles_data
|
||||
}
|
||||
)
|
||||
|
||||
def upload_mass_image(self, media_file):
|
||||
"""
|
||||
上传群发消息内的图片
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param media_file: 要上传的文件,一个 File-object
|
||||
:return: 上传成功时返回图片 URL
|
||||
"""
|
||||
res = self._post(
|
||||
url='https://api.weixin.qq.com/cgi-bin/media/uploadimg',
|
||||
files={
|
||||
'media': media_file
|
||||
},
|
||||
result_processor=lambda x: x['url']
|
||||
)
|
||||
return res
|
||||
@@ -1,235 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.exceptions import WeChatClientException
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMenu(BaseWeChatAPI):
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
查询自定义菜单。
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/ff9b7b85220e1396ffa16794a9d95adc.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
menu = client.menu.get()
|
||||
|
||||
"""
|
||||
try:
|
||||
return self._get('menu/get')
|
||||
except WeChatClientException as e:
|
||||
if e.errcode == 46003:
|
||||
# menu not exist
|
||||
return None
|
||||
else:
|
||||
raise e
|
||||
|
||||
def create(self, menu_data):
|
||||
"""
|
||||
创建自定义菜单 ::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient("appid", "secret")
|
||||
client.menu.create({
|
||||
"button":[
|
||||
{
|
||||
"type":"click",
|
||||
"name":"今日歌曲",
|
||||
"key":"V1001_TODAY_MUSIC"
|
||||
},
|
||||
{
|
||||
"type":"click",
|
||||
"name":"歌手简介",
|
||||
"key":"V1001_TODAY_SINGER"
|
||||
},
|
||||
{
|
||||
"name":"菜单",
|
||||
"sub_button":[
|
||||
{
|
||||
"type":"xml",
|
||||
"name":"搜索",
|
||||
"url":"http://www.soso.com/"
|
||||
},
|
||||
{
|
||||
"type":"xml",
|
||||
"name":"视频",
|
||||
"url":"http://v.qq.com/"
|
||||
},
|
||||
{
|
||||
"type":"click",
|
||||
"name":"赞一下我们",
|
||||
"key":"V1001_GOOD"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
详情请参考
|
||||
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421141013
|
||||
|
||||
:param menu_data: Python 字典
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'menu/create',
|
||||
data=menu_data
|
||||
)
|
||||
|
||||
update = create
|
||||
|
||||
def delete(self):
|
||||
"""
|
||||
删除自定义菜单。
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/8ed41ba931e4845844ad6d1eeb8060c8.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.menu.delete()
|
||||
|
||||
"""
|
||||
return self._get('menu/delete')
|
||||
|
||||
def get_menu_info(self):
|
||||
"""
|
||||
获取自定义菜单配置
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/17/4dc4b0514fdad7a5fbbd477aa9aab5ed.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
menu_info = client.menu.get_menu_info()
|
||||
|
||||
"""
|
||||
return self._get('get_current_selfmenu_info')
|
||||
|
||||
def add_conditional(self, menu_data):
|
||||
"""
|
||||
创建个性化菜单 ::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient("appid", "secret")
|
||||
client.menu.add_conditional({
|
||||
"button":[
|
||||
{
|
||||
"type":"click",
|
||||
"name":"今日歌曲",
|
||||
"key":"V1001_TODAY_MUSIC"
|
||||
},
|
||||
{
|
||||
"type":"click",
|
||||
"name":"歌手简介",
|
||||
"key":"V1001_TODAY_SINGER"
|
||||
},
|
||||
{
|
||||
"name":"菜单",
|
||||
"sub_button":[
|
||||
{
|
||||
"type":"xml",
|
||||
"name":"搜索",
|
||||
"url":"http://www.soso.com/"
|
||||
},
|
||||
{
|
||||
"type":"xml",
|
||||
"name":"视频",
|
||||
"url":"http://v.qq.com/"
|
||||
},
|
||||
{
|
||||
"type":"click",
|
||||
"name":"赞一下我们",
|
||||
"key":"V1001_GOOD"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"matchrule":{
|
||||
"group_id":"2",
|
||||
"sex":"1",
|
||||
"country":"中国",
|
||||
"province":"广东",
|
||||
"city":"广州",
|
||||
"client_platform_type":"2"
|
||||
}
|
||||
})
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
|
||||
|
||||
:param menu_data: Python 字典
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'menu/addconditional',
|
||||
data=menu_data
|
||||
)
|
||||
|
||||
def del_conditional(self, menu_id):
|
||||
"""
|
||||
删除个性化菜单
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
|
||||
|
||||
:param menu_id: 菜单ID
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.menu.del_conditional('menu_id')
|
||||
|
||||
"""
|
||||
return self._post(
|
||||
'menu/delconditional',
|
||||
data={'menuid': menu_id}
|
||||
)
|
||||
|
||||
def try_match(self, user_id):
|
||||
"""
|
||||
测试个性化菜单匹配结果
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/c48ccd12b69ae023159b4bfaa7c39c20.html
|
||||
|
||||
:param user_id: 可以是粉丝的OpenID,也可以是粉丝的微信号。
|
||||
|
||||
:return: 该接口将返回菜单配置
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.menu.try_match('openid')
|
||||
|
||||
"""
|
||||
return self._post(
|
||||
'menu/trymatch',
|
||||
data={'user_id': user_id}
|
||||
)
|
||||
@@ -1,72 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
from wechatpy.client.api.merchant.category import MerchantCategory
|
||||
from wechatpy.client.api.merchant.stock import MerchantStock
|
||||
from wechatpy.client.api.merchant.express import MerchantExpress
|
||||
from wechatpy.client.api.merchant.group import MerchantGroup
|
||||
from wechatpy.client.api.merchant.shelf import MerchantShelf
|
||||
from wechatpy.client.api.merchant.order import MerchantOrder
|
||||
from wechatpy.client.api.merchant.common import MerchantCommon
|
||||
|
||||
|
||||
class WeChatMerchant(BaseWeChatAPI):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(WeChatMerchant, self).__init__(*args, **kwargs)
|
||||
|
||||
# sub APIs
|
||||
self.category = MerchantCategory(self._client)
|
||||
self.stock = MerchantStock(self._client)
|
||||
self.express = MerchantExpress(self._client)
|
||||
self.group = MerchantGroup(self._client)
|
||||
self.shelf = MerchantShelf(self._client)
|
||||
self.order = MerchantOrder(self._client)
|
||||
self.common = MerchantCommon(self._client)
|
||||
|
||||
def create(self, product_data):
|
||||
return self._post(
|
||||
'merchant/create',
|
||||
data=product_data
|
||||
)
|
||||
|
||||
def delete(self, product_id):
|
||||
return self._post(
|
||||
'merchant/del',
|
||||
data={
|
||||
'product_id': product_id
|
||||
}
|
||||
)
|
||||
|
||||
def update(self, product_id, product_data):
|
||||
product_data['product_id'] = product_id
|
||||
return self._post(
|
||||
'merchant/update',
|
||||
data=product_data
|
||||
)
|
||||
|
||||
def get(self, product_id):
|
||||
return self._post(
|
||||
'merchant/get',
|
||||
data={
|
||||
'product_id': product_id
|
||||
}
|
||||
)
|
||||
|
||||
def get_by_status(self, status):
|
||||
return self._post(
|
||||
'merchant/getbystatus',
|
||||
data={
|
||||
'status': status
|
||||
}
|
||||
)
|
||||
|
||||
def update_product_status(self, product_id, status):
|
||||
return self._post(
|
||||
'merchant/modproductstatus',
|
||||
data={
|
||||
'product_id': product_id,
|
||||
'status': status
|
||||
}
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class MerchantCategory(BaseWeChatAPI):
|
||||
|
||||
def get_sub_categories(self, cate_id):
|
||||
res = self._post(
|
||||
'merchant/category/getsub',
|
||||
data={'cate_id': cate_id},
|
||||
result_processor=lambda x: x['cate_list']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_sku_list(self, cate_id):
|
||||
res = self._post(
|
||||
'merchant/category/getsku',
|
||||
data={'cate_id': cate_id},
|
||||
result_processor=lambda x: x['sku_table']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_properties(self, cate_id):
|
||||
res = self._post(
|
||||
'merchant/category/getproperty',
|
||||
data={'cate_id': cate_id},
|
||||
result_processor=lambda x: x['properties']
|
||||
)
|
||||
return res
|
||||
@@ -1,17 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class MerchantCommon(BaseWeChatAPI):
|
||||
|
||||
def upload_image(self, filename, image_data):
|
||||
res = self._post(
|
||||
'merchant/common/upload_img',
|
||||
params={
|
||||
'filename': filename
|
||||
},
|
||||
data=image_data,
|
||||
result_processor=lambda x: x['image_url']
|
||||
)
|
||||
return res
|
||||
@@ -1,48 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class MerchantExpress(BaseWeChatAPI):
|
||||
|
||||
def add(self, delivery_template):
|
||||
return self._post(
|
||||
'merchant/express/add',
|
||||
data={
|
||||
'delivery_template': delivery_template
|
||||
}
|
||||
)
|
||||
|
||||
def delete(self, template_id):
|
||||
return self._post(
|
||||
'merchant/express/del',
|
||||
data={
|
||||
'template_id': template_id
|
||||
}
|
||||
)
|
||||
|
||||
def update(self, template_id, delivery_template):
|
||||
return self._post(
|
||||
'merchant/express/update',
|
||||
data={
|
||||
'template_id': template_id,
|
||||
'delivery_template': delivery_template
|
||||
}
|
||||
)
|
||||
|
||||
def get(self, template_id):
|
||||
res = self._post(
|
||||
'merchant/express/getbyid',
|
||||
data={
|
||||
'template_id': template_id
|
||||
},
|
||||
result_processor=lambda x: x['template_info']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_all(self):
|
||||
res = self._get(
|
||||
'merchant/express/getall',
|
||||
result_processor=lambda x: x['template_info']
|
||||
)
|
||||
return res
|
||||
@@ -1,60 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class MerchantGroup(BaseWeChatAPI):
|
||||
|
||||
def add(self, name, product_list):
|
||||
return self._post(
|
||||
'merchant/group/add',
|
||||
data={
|
||||
'group_detail': {
|
||||
'group_name': name,
|
||||
'product_list': product_list
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def delete(self, group_id):
|
||||
return self._post(
|
||||
'merchant/group/del',
|
||||
data={
|
||||
'group_id': group_id
|
||||
}
|
||||
)
|
||||
|
||||
def update(self, group_id, name):
|
||||
return self._post(
|
||||
'merchant/group/propertymod',
|
||||
data={
|
||||
'group_id': group_id,
|
||||
'group_name': name
|
||||
}
|
||||
)
|
||||
|
||||
def update_product(self, group_id, product):
|
||||
return self._post(
|
||||
'merchant/group/productmod',
|
||||
data={
|
||||
'group_id': group_id,
|
||||
'product': product
|
||||
}
|
||||
)
|
||||
|
||||
def get_all(self):
|
||||
res = self._get(
|
||||
'merchant/group/getall',
|
||||
result_processor=lambda x: x['group_detail']
|
||||
)
|
||||
return res
|
||||
|
||||
def get(self, group_id):
|
||||
res = self._post(
|
||||
'merchant/group/getbyid',
|
||||
data={
|
||||
'group_id': group_id
|
||||
},
|
||||
result_processor=lambda x: x['group_detail']
|
||||
)
|
||||
return res
|
||||
@@ -1,52 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class MerchantOrder(BaseWeChatAPI):
|
||||
|
||||
def get(self, order_id):
|
||||
res = self._post(
|
||||
'merchant/order/getbyid',
|
||||
data={
|
||||
'order_id': order_id
|
||||
},
|
||||
result_processor=lambda x: x['order']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_by_filter(self, status=None, begin_time=None, end_time=None):
|
||||
filter_dict = optionaldict(
|
||||
status=status,
|
||||
begintime=begin_time,
|
||||
endtime=end_time
|
||||
)
|
||||
|
||||
res = self._post(
|
||||
'merchant/order/getbyfilter',
|
||||
data=dict(filter_dict),
|
||||
result_processor=lambda x: x['order_list']
|
||||
)
|
||||
return res
|
||||
|
||||
def set_delivery(self, order_id, company, track_no,
|
||||
need_delivery=1, is_others=0):
|
||||
return self._post(
|
||||
'merchant/order/setdelivery',
|
||||
data={
|
||||
'order_id': order_id,
|
||||
'delivery_company': company,
|
||||
'delivery_track_no': track_no,
|
||||
'need_delivery': need_delivery,
|
||||
'is_others': is_others
|
||||
}
|
||||
)
|
||||
|
||||
def close(self, order_id):
|
||||
return self._post(
|
||||
'merchant/order/close',
|
||||
data={
|
||||
'order_id': order_id
|
||||
}
|
||||
)
|
||||
@@ -1,50 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class MerchantShelf(BaseWeChatAPI):
|
||||
|
||||
def add(self, name, banner, shelf_data):
|
||||
return self._post(
|
||||
'merchant/shelf/add',
|
||||
data={
|
||||
'shelf_name': name,
|
||||
'shelf_banner': banner,
|
||||
'shelf_data': shelf_data
|
||||
}
|
||||
)
|
||||
|
||||
def delete(self, shelf_id):
|
||||
return self._post(
|
||||
'merchant/shelf/del',
|
||||
data={
|
||||
'shelf_id': shelf_id
|
||||
}
|
||||
)
|
||||
|
||||
def update(self, shelf_id, name, banner, shelf_data):
|
||||
return self._post(
|
||||
'merchant/shelf/add',
|
||||
data={
|
||||
'shelf_id': shelf_id,
|
||||
'shelf_name': name,
|
||||
'shelf_banner': banner,
|
||||
'shelf_data': shelf_data
|
||||
}
|
||||
)
|
||||
|
||||
def get_all(self):
|
||||
res = self._get(
|
||||
'merchant/shelf/getall',
|
||||
result_processor=lambda x: x['shelves']
|
||||
)
|
||||
return res
|
||||
|
||||
def get(self, shelf_id):
|
||||
return self._post(
|
||||
'merchant/shelf/getbyid',
|
||||
data={
|
||||
'shelf_id': shelf_id
|
||||
}
|
||||
)
|
||||
@@ -1,26 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class MerchantStock(BaseWeChatAPI):
|
||||
|
||||
def add(self, product_id, quantity, sku_info=''):
|
||||
return self._post(
|
||||
'merchant/stock/add',
|
||||
data={
|
||||
'product_id': product_id,
|
||||
'quantity': quantity,
|
||||
'sku_info': sku_info
|
||||
}
|
||||
)
|
||||
|
||||
def reduce(self, product_id, quantity, sku_info=''):
|
||||
return self._post(
|
||||
'merchant/stock/reduce',
|
||||
data={
|
||||
'product_id': product_id,
|
||||
'quantity': quantity,
|
||||
'sku_info': sku_info
|
||||
}
|
||||
)
|
||||
@@ -1,572 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import re
|
||||
import six
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMessage(BaseWeChatAPI):
|
||||
OPENID_RE = re.compile(r'^[\w\-]{28}$', re.I)
|
||||
|
||||
def _send_custom_message(self, data, account=None):
|
||||
data = data or {}
|
||||
if account:
|
||||
data['customservice'] = {'kf_account': account}
|
||||
return self._post(
|
||||
'message/custom/send',
|
||||
data=data
|
||||
)
|
||||
|
||||
def send_text(self, user_id, content, account=None):
|
||||
"""
|
||||
发送文本消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/7/12a5a320ae96fecdf0e15cb06123de9f.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param content: 消息正文
|
||||
:param account: 可选,客服账号
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.message.send_text('openid', 'text')
|
||||
|
||||
"""
|
||||
data = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'text',
|
||||
'text': {'content': content}
|
||||
}
|
||||
return self._send_custom_message(data, account=account)
|
||||
|
||||
def send_image(self, user_id, media_id, account=None):
|
||||
"""
|
||||
发送图片消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/7/12a5a320ae96fecdf0e15cb06123de9f.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param media_id: 图片的媒体ID。 可以通过 :func:`upload_media` 上传。
|
||||
:param account: 可选,客服账号
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.message.send_image('openid', 'media_id')
|
||||
|
||||
"""
|
||||
data = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'image',
|
||||
'image': {
|
||||
'media_id': media_id
|
||||
}
|
||||
}
|
||||
return self._send_custom_message(data, account=account)
|
||||
|
||||
def send_voice(self, user_id, media_id, account=None):
|
||||
"""
|
||||
发送语音消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/7/12a5a320ae96fecdf0e15cb06123de9f.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param media_id: 发送的语音的媒体ID。 可以通过 :func:`upload_media` 上传。
|
||||
:param account: 可选,客服账号
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.message.send_voice('openid', 'media_id')
|
||||
|
||||
"""
|
||||
data = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'voice',
|
||||
'voice': {
|
||||
'media_id': media_id
|
||||
}
|
||||
}
|
||||
return self._send_custom_message(data, account=account)
|
||||
|
||||
def send_video(self, user_id, media_id, title=None,
|
||||
description=None, account=None):
|
||||
"""
|
||||
发送视频消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/7/12a5a320ae96fecdf0e15cb06123de9f.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param media_id: 发送的视频的媒体ID。 可以通过 :func:`upload_media` 上传。
|
||||
:param title: 视频消息的标题
|
||||
:param description: 视频消息的描述
|
||||
:param account: 可选,客服账号
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.message.send_video('openid', 'media_id', 'title', 'description')
|
||||
"""
|
||||
video_data = {
|
||||
'media_id': media_id,
|
||||
}
|
||||
if title:
|
||||
video_data['title'] = title
|
||||
if description:
|
||||
video_data['description'] = description
|
||||
|
||||
data = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'video',
|
||||
'video': video_data
|
||||
}
|
||||
return self._send_custom_message(data, account=account)
|
||||
|
||||
def send_music(self, user_id, url, hq_url, thumb_media_id,
|
||||
title=None, description=None, account=None):
|
||||
"""
|
||||
发送音乐消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/7/12a5a320ae96fecdf0e15cb06123de9f.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param url: 音乐链接
|
||||
:param hq_url: 高品质音乐链接,wifi环境优先使用该链接播放音乐
|
||||
:param thumb_media_id: 缩略图的媒体ID。 可以通过 :func:`upload_media` 上传。
|
||||
:param title: 音乐标题
|
||||
:param description: 音乐描述
|
||||
:param account: 可选,客服账号
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
music_data = {
|
||||
'musicurl': url,
|
||||
'hqmusicurl': hq_url,
|
||||
'thumb_media_id': thumb_media_id
|
||||
}
|
||||
if title:
|
||||
music_data['title'] = title
|
||||
if description:
|
||||
music_data['description'] = description
|
||||
|
||||
data = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'music',
|
||||
'music': music_data
|
||||
}
|
||||
return self._send_custom_message(data, account=account)
|
||||
|
||||
def send_articles(self, user_id, articles, account=None):
|
||||
"""
|
||||
发送图文消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/7/12a5a320ae96fecdf0e15cb06123de9f.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param articles: 一个包含至多10个图文的数组, 或者微信图文消息素材 media_id
|
||||
:param account: 可选,客服账号
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
if isinstance(articles, (tuple, list)):
|
||||
articles_data = []
|
||||
for article in articles:
|
||||
articles_data.append({
|
||||
'title': article['title'],
|
||||
'description': article['description'],
|
||||
'url': article['url'],
|
||||
'picurl': article.get('image', article.get('picurl')),
|
||||
})
|
||||
data = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'news',
|
||||
'news': {
|
||||
'articles': articles_data
|
||||
}
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'mpnews',
|
||||
'mpnews': {
|
||||
'media_id': articles,
|
||||
}
|
||||
}
|
||||
return self._send_custom_message(data, account=account)
|
||||
|
||||
def send_card(self, user_id, card_id, card_ext, account=None):
|
||||
"""
|
||||
发送卡券消息
|
||||
|
||||
详情请参参考
|
||||
http://mp.weixin.qq.com/wiki/1/70a29afed17f56d537c833f89be979c9.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param card_id: 卡券 ID
|
||||
:param card_ext: 卡券扩展信息
|
||||
:param account: 可选,客服账号
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = {
|
||||
'touser': user_id,
|
||||
'msgtype': 'wxcard',
|
||||
'wxcard': {
|
||||
'card_id': card_id,
|
||||
'card_ext': card_ext
|
||||
}
|
||||
}
|
||||
return self._send_custom_message(data, account=account)
|
||||
|
||||
def delete_mass(self, msg_id):
|
||||
"""
|
||||
删除群发消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param msg_id: 要删除的群发消息 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.message.delete_mass('message id')
|
||||
|
||||
"""
|
||||
return self._post(
|
||||
'message/mass/delete',
|
||||
data={
|
||||
'msg_id': msg_id
|
||||
}
|
||||
)
|
||||
|
||||
def _send_mass_message(self, group_or_users, msg_type, msg,
|
||||
is_to_all=False, preview=False):
|
||||
data = {
|
||||
'msgtype': msg_type
|
||||
}
|
||||
if not preview:
|
||||
if isinstance(group_or_users, (tuple, list)):
|
||||
# send by user ids
|
||||
data['touser'] = group_or_users
|
||||
endpoint = 'message/mass/send'
|
||||
else:
|
||||
# send by group id
|
||||
data['filter'] = {
|
||||
'group_id': group_or_users,
|
||||
'is_to_all': is_to_all,
|
||||
}
|
||||
endpoint = 'message/mass/sendall'
|
||||
else:
|
||||
if not isinstance(group_or_users, six.string_types):
|
||||
raise ValueError('group_or_users should be string types')
|
||||
# 预览接口
|
||||
if self.OPENID_RE.match(group_or_users):
|
||||
# 按照 openid 预览群发
|
||||
data['touser'] = group_or_users
|
||||
else:
|
||||
# 按照微信号预览群发
|
||||
data['towxname'] = group_or_users
|
||||
endpoint = 'message/mass/preview'
|
||||
|
||||
data.update(msg)
|
||||
return self._post(
|
||||
endpoint,
|
||||
data=data
|
||||
)
|
||||
|
||||
def send_mass_text(self, group_or_users, content,
|
||||
is_to_all=False, preview=False):
|
||||
"""
|
||||
群发文本消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param group_or_users: 值为整型数字时为按分组群发,值为列表/元组时为按 OpenID 列表群发
|
||||
:param content: 消息正文
|
||||
:param is_to_all: 用于设定是否向全部用户发送,值为true或false,选择true该消息群发给所有用户
|
||||
选择false可根据group_id发送给指定群组的用户
|
||||
:param preview: 是否发送预览,此时 group_or_users 参数应为一个openid字符串
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._send_mass_message(
|
||||
group_or_users,
|
||||
'text',
|
||||
{
|
||||
'text': {
|
||||
'content': content
|
||||
}
|
||||
},
|
||||
is_to_all,
|
||||
preview
|
||||
)
|
||||
|
||||
def send_mass_image(self, group_or_users, media_id,
|
||||
is_to_all=False, preview=False):
|
||||
"""
|
||||
群发图片消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param group_or_users: 值为整型数字时为按分组群发,值为列表/元组时为按 OpenID 列表群发
|
||||
:param media_id: 图片的媒体 ID。 可以通过 :func:`upload_media` 上传。
|
||||
:param is_to_all: 用于设定是否向全部用户发送,值为true或false,选择true该消息群发给所有用户
|
||||
选择false可根据group_id发送给指定群组的用户
|
||||
:param preview: 是否发送预览,此时 group_or_users 参数应为一个openid字符串
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._send_mass_message(
|
||||
group_or_users,
|
||||
'image',
|
||||
{
|
||||
'image': {
|
||||
'media_id': media_id
|
||||
}
|
||||
},
|
||||
is_to_all,
|
||||
preview
|
||||
)
|
||||
|
||||
def send_mass_voice(self, group_or_users, media_id,
|
||||
is_to_all=False, preview=False):
|
||||
"""
|
||||
群发语音消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param group_or_users: 值为整型数字时为按分组群发,值为列表/元组时为按 OpenID 列表群发
|
||||
:param media_id: 语音的媒体 ID。可以通过 :func:`upload_media` 上传。
|
||||
:param is_to_all: 用于设定是否向全部用户发送,值为true或false,选择true该消息群发给所有用户
|
||||
选择false可根据group_id发送给指定群组的用户
|
||||
:param preview: 是否发送预览,此时 group_or_users 参数应为一个openid字符串
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._send_mass_message(
|
||||
group_or_users,
|
||||
'voice',
|
||||
{
|
||||
'voice': {
|
||||
'media_id': media_id
|
||||
}
|
||||
},
|
||||
is_to_all,
|
||||
preview
|
||||
)
|
||||
|
||||
def send_mass_video(self, group_or_users, media_id, title=None,
|
||||
description=None, is_to_all=False, preview=False):
|
||||
"""
|
||||
群发视频消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param group_or_users: 值为整型数字时为按分组群发,值为列表/元组时为按 OpenID 列表群发
|
||||
:param media_id: 视频的媒体 ID。可以通过 :func:`upload_video` 上传。
|
||||
:param title: 视频标题
|
||||
:param description: 视频描述
|
||||
:param is_to_all: 用于设定是否向全部用户发送,值为true或false,选择true该消息群发给所有用户
|
||||
选择false可根据group_id发送给指定群组的用户
|
||||
:param preview: 是否发送预览,此时 group_or_users 参数应为一个openid字符串
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
video_data = {
|
||||
'media_id': media_id
|
||||
}
|
||||
if title:
|
||||
video_data['title'] = title
|
||||
if description:
|
||||
video_data['description'] = description
|
||||
return self._send_mass_message(
|
||||
group_or_users,
|
||||
'mpvideo',
|
||||
{
|
||||
'mpvideo': video_data
|
||||
},
|
||||
is_to_all,
|
||||
preview
|
||||
)
|
||||
|
||||
def send_mass_article(self, group_or_users, media_id,
|
||||
is_to_all=False, preview=False):
|
||||
"""
|
||||
群发图文消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param group_or_users: 值为整型数字时为按分组群发,值为列表/元组时为按 OpenID 列表群发
|
||||
:param media_id: 图文的媒体 ID。可以通过 :func:`upload_articles` 上传。
|
||||
:param is_to_all: 用于设定是否向全部用户发送,值为true或false,选择true该消息群发给所有用户
|
||||
选择false可根据group_id发送给指定群组的用户
|
||||
:param preview: 是否发送预览,此时 group_or_users 参数应为一个openid字符串
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._send_mass_message(
|
||||
group_or_users,
|
||||
'mpnews',
|
||||
{
|
||||
'mpnews': {
|
||||
'media_id': media_id
|
||||
}
|
||||
},
|
||||
is_to_all,
|
||||
preview
|
||||
)
|
||||
|
||||
def get_mass(self, msg_id):
|
||||
"""
|
||||
查询群发消息发送状态
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param msg_id: 群发消息后返回的消息id
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.message.get_mass('mass message id')
|
||||
|
||||
"""
|
||||
return self._post(
|
||||
'message/mass/get',
|
||||
data={
|
||||
'msg_id': msg_id
|
||||
}
|
||||
)
|
||||
|
||||
def send_template(self, user_id, template_id, url, top_color, data):
|
||||
"""
|
||||
发送模板消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/17/304c1885ea66dbedf7dc170d84999a9d.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param template_id: 模板 ID。在公众平台线上模板库中选用模板获得
|
||||
:param url: 链接地址
|
||||
:param top_color: 消息顶部颜色
|
||||
:param data: 模板消息数据
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'message/template/send',
|
||||
data={
|
||||
'touser': user_id,
|
||||
'template_id': template_id,
|
||||
'url': url,
|
||||
'topcolor': top_color,
|
||||
'data': data
|
||||
}
|
||||
)
|
||||
|
||||
def send_template_applet(self, user_id, template_id, appid, pagepath, data):
|
||||
"""
|
||||
发送模板消息,点击跳转到小程序
|
||||
|
||||
详情请参考
|
||||
https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433751277
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param template_id: 模板 ID。在公众平台线上模板库中选用模板获得
|
||||
:param appid: 所需跳转到的小程序appid(该小程序appid必须与发模板消息的公众号是绑定关联关系,暂不支持小游戏)
|
||||
:param pagepath: 所需跳转到小程序的具体页面路径,支持带参数,(示例index?foo=bar),要求该小程序已发布,暂不支持小游戏 不必填
|
||||
:param data: 模板消息数据
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data_all = {
|
||||
'touser': user_id,
|
||||
'template_id': template_id,
|
||||
"miniprogram": {
|
||||
"appid": appid,
|
||||
# "pagepath": pagepath
|
||||
},
|
||||
'data': data
|
||||
}
|
||||
if pagepath:
|
||||
data_all['miniprogram']['pagepath'] = pagepath
|
||||
return self._post(
|
||||
'message/template/send',
|
||||
data=data_all
|
||||
)
|
||||
|
||||
def get_autoreply_info(self):
|
||||
"""
|
||||
获取自动回复规则
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/7/7b5789bb1262fb866d01b4b40b0efecb.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
info = client.message.get_autoreply_info()
|
||||
|
||||
"""
|
||||
return self._get('get_current_autoreply_info')
|
||||
|
||||
def send_mass_card(self, group_or_users, card_id,
|
||||
is_to_all=False, preview=False):
|
||||
"""
|
||||
群发卡券消息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
|
||||
:param group_or_users: 值为整型数字时为按分组群发,值为列表/元组时为按 OpenID 列表群发
|
||||
:param card_id: 卡券 ID
|
||||
:param is_to_all: 用于设定是否向全部用户发送,值为true或false,选择true该消息群发给所有用户
|
||||
选择false可根据group_id发送给指定群组的用户
|
||||
:param preview: 是否发送预览,此时 group_or_users 参数应为一个openid字符串
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._send_mass_message(
|
||||
group_or_users,
|
||||
'wxcard',
|
||||
{
|
||||
'wxcard': {
|
||||
'card_id': card_id
|
||||
}
|
||||
},
|
||||
is_to_all,
|
||||
preview
|
||||
)
|
||||
@@ -1,51 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMisc(BaseWeChatAPI):
|
||||
|
||||
def short_url(self, long_url):
|
||||
"""
|
||||
将一条长链接转成短链接
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/10/165c9b15eddcfbd8699ac12b0bd89ae6.html
|
||||
|
||||
:param long_url: 长链接地址
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.misc.short_url('http://www.qq.com')
|
||||
|
||||
"""
|
||||
return self._post(
|
||||
'shorturl',
|
||||
data={
|
||||
'action': 'long2short',
|
||||
'long_url': long_url
|
||||
}
|
||||
)
|
||||
|
||||
def get_wechat_ips(self):
|
||||
"""
|
||||
获取微信服务器 IP 地址列表
|
||||
|
||||
:return: IP 地址列表
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
ips = client.misc.get_wechat_ips()
|
||||
|
||||
"""
|
||||
res = self._get(
|
||||
'getcallbackip',
|
||||
result_processor=lambda x: x['ip_list']
|
||||
)
|
||||
return res
|
||||
@@ -1,97 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatPoi(BaseWeChatAPI):
|
||||
|
||||
def add_picture(self, access_token, buffer):
|
||||
"""
|
||||
上传图片接口
|
||||
:param access_token: 接口凭证
|
||||
:param buffer: buffer
|
||||
:return:
|
||||
详情请参考: https://mp.weixin.qq.com/wiki/16/8f182af4d8dcea02c56506306bdb2f4c.html
|
||||
"""
|
||||
|
||||
def add(self, poi_data):
|
||||
"""
|
||||
创建门店
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/8f182af4d8dcea02c56506306bdb2f4c.html
|
||||
|
||||
:param poi_data: 门店信息字典
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post('poi/addpoi', data=poi_data)
|
||||
|
||||
def get(self, poi_id):
|
||||
"""
|
||||
查询门店信息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/8f182af4d8dcea02c56506306bdb2f4c.html
|
||||
|
||||
:param poi_id: 门店 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post('poi/getpoi', data={'poi_id': poi_id})
|
||||
|
||||
def list(self, begin=0, limit=20):
|
||||
"""
|
||||
查询门店列表
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/8f182af4d8dcea02c56506306bdb2f4c.html
|
||||
|
||||
:param begin: 开始位置,0 即为从第一条开始查询
|
||||
:param limit: 返回数据条数,最大允许50,默认为20
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'poi/getpoilist',
|
||||
data={
|
||||
'begin': begin,
|
||||
'limit': limit,
|
||||
}
|
||||
)
|
||||
|
||||
def update(self, poi_data):
|
||||
"""
|
||||
修改门店
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/8f182af4d8dcea02c56506306bdb2f4c.html
|
||||
|
||||
:param poi_data: 门店信息字典
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post('poi/updatepoi', data=poi_data)
|
||||
|
||||
def delete(self, poi_id):
|
||||
"""
|
||||
删除门店
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/8f182af4d8dcea02c56506306bdb2f4c.html
|
||||
|
||||
:param poi_id: 门店 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post('poi/delpoi', data={'poi_id': poi_id})
|
||||
|
||||
def get_categories(self):
|
||||
"""
|
||||
获取微信门店类目表
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/16/8f182af4d8dcea02c56506306bdb2f4c.html
|
||||
|
||||
:return: 门店类目表
|
||||
"""
|
||||
res = self._get(
|
||||
'api_getwxcategory',
|
||||
result_processor=lambda x: x['category_list']
|
||||
)
|
||||
return res
|
||||
@@ -1,87 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import requests
|
||||
import six
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatQRCode(BaseWeChatAPI):
|
||||
|
||||
def create(self, qrcode_data):
|
||||
"""
|
||||
创建二维码
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/18/28fc21e7ed87bec960651f0ce873ef8a.html
|
||||
|
||||
:param data: 你要发送的参数 dict
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.qrcode.create({
|
||||
'expire_seconds': 1800,
|
||||
'action_name': 'QR_SCENE',
|
||||
'action_info': {
|
||||
'scene': {'scene_id': 123},
|
||||
}
|
||||
})
|
||||
|
||||
"""
|
||||
return self._post(
|
||||
'qrcode/create',
|
||||
data=qrcode_data
|
||||
)
|
||||
|
||||
def show(self, ticket):
|
||||
"""
|
||||
通过ticket换取二维码
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/18/28fc21e7ed87bec960651f0ce873ef8a.html
|
||||
|
||||
:param ticket: 二维码 ticket 。可以通过 :func:`create` 获取到
|
||||
:return: 返回的 Request 对象
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.qrcode.show('ticket data')
|
||||
|
||||
"""
|
||||
if isinstance(ticket, dict):
|
||||
ticket = ticket['ticket']
|
||||
return requests.get(
|
||||
url='https://mp.weixin.qq.com/cgi-bin/showqrcode',
|
||||
params={
|
||||
'ticket': ticket
|
||||
}
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_url(cls, ticket):
|
||||
"""
|
||||
通过ticket换取二维码地址
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/18/28fc21e7ed87bec960651f0ce873ef8a.html
|
||||
|
||||
:param ticket: 二维码 ticket 。可以通过 :func:`create` 获取到
|
||||
:return: 返回的二维码地址
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
url = client.qrcode.get_url('ticket data')
|
||||
|
||||
"""
|
||||
url = 'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={ticket}'
|
||||
if isinstance(ticket, dict):
|
||||
ticket = ticket['ticket']
|
||||
ticket = six.moves.urllib.parse.quote(ticket)
|
||||
return url.format(ticket=ticket)
|
||||
@@ -1,171 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatScan(BaseWeChatAPI):
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/scan/'
|
||||
|
||||
def get_merchant_info(self):
|
||||
"""
|
||||
获取商户信息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/6/c61604ff6890d386d6227945ad4a68d2.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
info = client.scan.get_merchant_info()
|
||||
"""
|
||||
return self._get('merchantinfo/get')
|
||||
|
||||
def create_product(self, product_data):
|
||||
"""
|
||||
创建商品
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/6/c61604ff6890d386d6227945ad4a68d2.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post('product/create', data=product_data)
|
||||
|
||||
def modify_product_status(self, standard, key, status):
|
||||
"""
|
||||
提交审核/取消发布商品
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/1007691d0f1c10a0588c6517f12ed70f.html
|
||||
|
||||
:param standard: 商品编码标准
|
||||
:param key: 商品编码内容
|
||||
:param status: 设置发布状态。on 为提交审核,off 为取消发布
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = {
|
||||
'keystandard': standard,
|
||||
'keystr': key,
|
||||
'status': status,
|
||||
}
|
||||
return self._post('product/modstatus', data=data)
|
||||
|
||||
def publish_product(self, standard, key):
|
||||
"""
|
||||
提交审核商品 shortcut 接口
|
||||
|
||||
等同于调用 ``modify_product_status(standard, key, 'on')``
|
||||
"""
|
||||
return self.modify_product_status(standard, key, 'on')
|
||||
|
||||
def unpublish_product(self, standard, key):
|
||||
"""
|
||||
取消发布商品 shortcut 接口
|
||||
|
||||
等同于调用 ``modify_product_status(standard, key, 'off')``
|
||||
"""
|
||||
return self.modify_product_status(standard, key, 'off')
|
||||
|
||||
def set_test_whitelist(self, userids=None, usernames=None):
|
||||
"""
|
||||
设置测试人员白名单
|
||||
|
||||
注意:每次设置均被视为一次重置,而非增量设置。openid、微信号合计最多设置10个。
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/1007691d0f1c10a0588c6517f12ed70f.html
|
||||
|
||||
:param userids: 可选,测试人员的 openid 列表
|
||||
:param usernames: 可选,测试人员的微信号列表
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict(
|
||||
openid=userids,
|
||||
username=usernames
|
||||
)
|
||||
return self._post('testwhitelist/set', data=data)
|
||||
|
||||
def get_product(self, standard, key):
|
||||
"""
|
||||
查询商品信息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/7fa787701295b884410b5163e13313af.html
|
||||
|
||||
:param standard: 商品编码标准
|
||||
:param key: 商品编码内容
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = {
|
||||
'keystandard': standard,
|
||||
'keystr': key,
|
||||
}
|
||||
return self._post('product/get', data=data)
|
||||
|
||||
def list_product(self, offset=0, limit=10, status=None, key=None):
|
||||
"""
|
||||
批量查询商品信息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/7fa787701295b884410b5163e13313af.html
|
||||
|
||||
:param offset: 可选,批量查询的起始位置,从 0 开始,包含该起始位置
|
||||
:param limit: 可选,批量查询的数量,默认为 10
|
||||
:param status: 可选,支持按状态拉取。on为发布状态,off为未发布状态,
|
||||
check为审核中状态,reject为审核未通过状态,all为所有状态
|
||||
:param key: 支持按部分编码内容拉取。填写该参数后,可将编码内容中包含所传参数的商品信息拉出
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict(
|
||||
offset=offset,
|
||||
limit=limit,
|
||||
status=status,
|
||||
keystr=key,
|
||||
)
|
||||
return self._post('product/getlist', data=data)
|
||||
|
||||
def update_product(self, product_data):
|
||||
"""
|
||||
更新商品信息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/7fa787701295b884410b5163e13313af.html
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post('product/update', data=product_data)
|
||||
|
||||
def clear_product(self, standard, key):
|
||||
"""
|
||||
清除商品信息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/7fa787701295b884410b5163e13313af.html
|
||||
|
||||
:param standard: 商品编码标准
|
||||
:param key: 商品编码内容
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = {
|
||||
'keystandard': standard,
|
||||
'keystr': key,
|
||||
}
|
||||
return self._post('product/clear', data=data)
|
||||
|
||||
def check_ticket(self, ticket):
|
||||
"""
|
||||
检查 wxticket 参数有效性
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/7fa787701295b884410b5163e13313af.html
|
||||
|
||||
:param ticket: 请求 URL 中带上的 wxticket 参数
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post('scanticket/check', data={'ticket': ticket})
|
||||
@@ -1,59 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatSemantic(BaseWeChatAPI):
|
||||
|
||||
def search(self,
|
||||
query,
|
||||
category,
|
||||
uid=None,
|
||||
latitude=None,
|
||||
longitude=None,
|
||||
city=None,
|
||||
region=None):
|
||||
"""
|
||||
发送语义理解请求
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/0ce78b3c9524811fee34aba3e33f3448.html
|
||||
|
||||
:param query: 输入文本串
|
||||
:param category: 需要使用的服务类型,多个可传入列表
|
||||
:param uid: 可选,用户唯一id(非开发者id),用户区分公众号下的不同用户(建议填入用户openid)
|
||||
:param latitude: 可选,纬度坐标,与经度同时传入;与城市二选一传入
|
||||
:param longitude: 可选,经度坐标,与纬度同时传入;与城市二选一传入
|
||||
:param city: 可选,城市名称,与经纬度二选一传入
|
||||
:param region: 可选,区域名称,在城市存在的情况下可省;与经纬度二选一传入
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
res = client.semantic.search(
|
||||
'查一下明天从北京到上海的南航机票',
|
||||
'flight,hotel',
|
||||
city='北京'
|
||||
)
|
||||
|
||||
"""
|
||||
if isinstance(category, (tuple, list)):
|
||||
category = ','.join(category)
|
||||
|
||||
data = optionaldict()
|
||||
data['query'] = query
|
||||
data['category'] = category
|
||||
data['uid'] = uid
|
||||
data['latitude'] = latitude
|
||||
data['longitude'] = longitude
|
||||
data['city'] = city
|
||||
data['region'] = region
|
||||
data['appid'] = self._client.appid
|
||||
return self._post(
|
||||
url='https://api.weixin.qq.com/semantic/semproxy/search',
|
||||
data=data
|
||||
)
|
||||
@@ -1,376 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
import six
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatShakeAround(BaseWeChatAPI):
|
||||
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/'
|
||||
|
||||
@classmethod
|
||||
def _to_timestamp(cls, date):
|
||||
if isinstance(date, six.string_types):
|
||||
date = datetime.strptime(date, '%Y-%m-%d %H:%M:%S')
|
||||
if isinstance(date, datetime):
|
||||
timestamp = int(time.mktime(date.timetuple()))
|
||||
return timestamp
|
||||
return int(date)
|
||||
|
||||
def apply_device_id(self, quantity, reason, poi_id=None, comment=None):
|
||||
"""
|
||||
申请设备ID
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/b9e012f917e3484b7ed02771156411f3.html
|
||||
|
||||
:param quantity: 申请的设备ID的数量,单次新增设备超过500个,需走人工审核流程
|
||||
:param reason: 申请理由,不超过100个字
|
||||
:param poi_id: 可选,设备关联的门店ID
|
||||
:param comment: 可选,备注,不超过15个汉字或30个英文字母
|
||||
:return: 申请的设备信息
|
||||
"""
|
||||
data = optionaldict()
|
||||
data['quantity'] = quantity
|
||||
data['apply_reason'] = reason
|
||||
data['poi_id'] = poi_id
|
||||
data['comment'] = comment
|
||||
res = self._post(
|
||||
'shakearound/device/applyid',
|
||||
data=data,
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def update_device(self, device_id=None, uuid=None, major=None,
|
||||
minor=None, comment=None):
|
||||
"""
|
||||
更新设备信息
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/b9e012f917e3484b7ed02771156411f3.html
|
||||
|
||||
:param device_id: 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先
|
||||
:param uuid: UUID
|
||||
:param major: major
|
||||
:param minor: minor
|
||||
:param comment: 设备的备注信息,不超过15个汉字或30个英文字母。
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict()
|
||||
data['comment'] = comment
|
||||
data['device_identifier'] = {
|
||||
'device_id': device_id,
|
||||
'uuid': uuid,
|
||||
'major': major,
|
||||
'minor': minor
|
||||
}
|
||||
return self._post(
|
||||
'shakearound/device/update',
|
||||
data=data
|
||||
)
|
||||
|
||||
def bind_device_location(self, poi_id, device_id=None, uuid=None,
|
||||
major=None, minor=None):
|
||||
"""
|
||||
配置设备与门店的关联关系
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/b9e012f917e3484b7ed02771156411f3.html
|
||||
|
||||
:param poi_id: 待关联的门店ID
|
||||
:param device_id: 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先
|
||||
:param uuid: UUID
|
||||
:param major: major
|
||||
:param minor: minor
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict()
|
||||
data['poi_id'] = poi_id
|
||||
data['device_identifier'] = {
|
||||
'device_id': device_id,
|
||||
'uuid': uuid,
|
||||
'major': major,
|
||||
'minor': minor
|
||||
}
|
||||
return self._post(
|
||||
'shakearound/device/bindlocation',
|
||||
data=data
|
||||
)
|
||||
|
||||
def search_device(self, identifiers=None, apply_id=None,
|
||||
begin=0, count=10):
|
||||
"""
|
||||
查询设备列表
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/b9e012f917e3484b7ed02771156411f3.html
|
||||
|
||||
:param identifiers: 设备 ID 信息列表
|
||||
:param apply_id: 批次ID,申请设备ID超出500个时所返回批次ID
|
||||
:param begin: 设备列表的起始索引值
|
||||
:param count: 待查询的设备个数
|
||||
:return: 设备列表
|
||||
"""
|
||||
data = optionaldict()
|
||||
data['begin'] = begin
|
||||
data['count'] = count
|
||||
data['apply_id'] = apply_id
|
||||
if identifiers:
|
||||
data['device_identifiers'] = identifiers
|
||||
res = self._post(
|
||||
'shakearound/device/search',
|
||||
data=data,
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def add_page(self, title, description, icon_url, page_url, comment=None):
|
||||
"""
|
||||
新增页面
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/5/6626199ea8757c752046d8e46cf13251.html
|
||||
|
||||
:param title: 在摇一摇页面展示的主标题,不超过6个字
|
||||
:param description: 在摇一摇页面展示的副标题,不超过7个字
|
||||
:param icon_url: 在摇一摇页面展示的图片。图片需先上传至微信侧服务器,
|
||||
用“素材管理-上传图片素材”接口上传图片,返回的图片URL再配置在此处
|
||||
:param page_url: 跳转链接
|
||||
:param comment: 可选,页面的备注信息,不超过15个字
|
||||
:return: 页面信息
|
||||
"""
|
||||
data = optionaldict()
|
||||
data['title'] = title
|
||||
data['description'] = description
|
||||
data['icon_url'] = icon_url
|
||||
data['page_url'] = page_url
|
||||
data['comment'] = comment
|
||||
res = self._post(
|
||||
'shakearound/page/add',
|
||||
data=data,
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def update_page(self, page_id, title, description,
|
||||
icon_url, page_url, comment=None):
|
||||
"""
|
||||
编辑页面信息
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/5/6626199ea8757c752046d8e46cf13251.html
|
||||
|
||||
:param page_id: 摇周边页面唯一ID
|
||||
:param title: 在摇一摇页面展示的主标题,不超过6个字
|
||||
:param description: 在摇一摇页面展示的副标题,不超过7个字
|
||||
:param icon_url: 在摇一摇页面展示的图片。图片需先上传至微信侧服务器,
|
||||
用“素材管理-上传图片素材”接口上传图片,返回的图片URL再配置在此处
|
||||
:param page_url: 跳转链接
|
||||
:param comment: 可选,页面的备注信息,不超过15个字
|
||||
:return: 页面信息
|
||||
"""
|
||||
data = optionaldict()
|
||||
data['page_id'] = page_id
|
||||
data['title'] = title
|
||||
data['description'] = description
|
||||
data['icon_url'] = icon_url
|
||||
data['page_url'] = page_url
|
||||
data['comment'] = comment
|
||||
res = self._post(
|
||||
'shakearound/page/update',
|
||||
data=data,
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def search_pages(self, page_ids=None, begin=0, count=10):
|
||||
"""
|
||||
查询页面列表
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/5/6626199ea8757c752046d8e46cf13251.html
|
||||
|
||||
:param page_ids: 指定页面的id列表
|
||||
:param begin: 页面列表的起始索引值
|
||||
:param count: 待查询的页面个数
|
||||
:return: 页面查询结果信息
|
||||
"""
|
||||
if not page_ids:
|
||||
data = {
|
||||
'type': 2,
|
||||
'begin': begin,
|
||||
'count': count
|
||||
}
|
||||
else:
|
||||
if not isinstance(page_ids, (tuple, list)):
|
||||
page_ids = [page_ids]
|
||||
data = {
|
||||
'type': 1,
|
||||
'page_ids': page_ids
|
||||
}
|
||||
|
||||
res = self._post(
|
||||
'shakearound/page/search',
|
||||
data=data,
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def delete_page(self, page_id):
|
||||
"""
|
||||
删除页面
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/5/6626199ea8757c752046d8e46cf13251.html
|
||||
|
||||
:param page_id: 指定页面的id列表
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'shakearound/page/delete',
|
||||
data={
|
||||
'page_id': page_id
|
||||
}
|
||||
)
|
||||
|
||||
def add_material(self, media_file, media_type='icon'):
|
||||
"""
|
||||
上传图片素材
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/5/e997428269ff189d8f9a4b9e177be2d9.html
|
||||
|
||||
:param media_file: 要上传的文件,一个 File-object
|
||||
:param media_type: 摇一摇素材类型, 取值为 icon或者 license, 默认 icon.
|
||||
:return: 上传的素材信息
|
||||
"""
|
||||
res = self._post(
|
||||
'shakearound/material/add',
|
||||
files={
|
||||
'media': media_file
|
||||
},
|
||||
params={
|
||||
'type': media_type
|
||||
},
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def bind_device_pages(self, page_ids, bind, append, device_id=None,
|
||||
uuid=None, major=None, minor=None):
|
||||
"""
|
||||
配置设备与页面的关联关系
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/12/c8120214ec0ba08af5dfcc0da1a11400.html
|
||||
|
||||
:param page_ids: 待关联的页面列表
|
||||
:param bind: 关联操作标志位, 0为解除关联关系,1为建立关联关系
|
||||
:param append: 新增操作标志位, 0为覆盖,1为新增
|
||||
:param device_id: 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先
|
||||
:param uuid: UUID
|
||||
:param major: major
|
||||
:param minor: minor
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
if not isinstance(page_ids, (tuple, list)):
|
||||
page_ids = [page_ids]
|
||||
data = {
|
||||
'page_ids': page_ids,
|
||||
'bind': int(bind),
|
||||
'append': int(append),
|
||||
'device_identifier': {
|
||||
'device_id': device_id,
|
||||
'uuid': uuid,
|
||||
'major': major,
|
||||
'minor': minor
|
||||
}
|
||||
}
|
||||
return self._post(
|
||||
'shakearound/device/bindpage',
|
||||
data=data
|
||||
)
|
||||
|
||||
def get_shake_info(self, ticket):
|
||||
"""
|
||||
获取摇周边的设备及用户信息
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/3/34904a5db3d0ec7bb5306335b8da1faf.html
|
||||
|
||||
:param ticket: 摇周边业务的ticket,可在摇到的URL中得到,ticket生效时间为30分钟
|
||||
:return: 设备及用户信息
|
||||
"""
|
||||
res = self._post(
|
||||
'shakearound/user/getshakeinfo',
|
||||
data={
|
||||
'ticket': ticket
|
||||
},
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_device_statistics(self, begin_date, end_date, device_id=None,
|
||||
uuid=None, major=None, minor=None):
|
||||
"""
|
||||
以设备为维度的数据统计接口
|
||||
http://mp.weixin.qq.com/wiki/0/8a24bcacad40fe7ee98d1573cb8a6764.html
|
||||
|
||||
:param begin_date: 起始时间,最长时间跨度为30天
|
||||
:param end_date: 结束时间,最长时间跨度为30天
|
||||
:param device_id: 设备编号,若填了UUID、major、minor,则可不填设备编号,若二者都填,则以设备编号为优先
|
||||
:param uuid: UUID
|
||||
:param major: major
|
||||
:param minor: minor
|
||||
"""
|
||||
data = {
|
||||
'device_identifier': {
|
||||
'device_id': device_id,
|
||||
'uuid': uuid,
|
||||
'major': major,
|
||||
'minor': minor
|
||||
},
|
||||
'begin_date': self._to_timestamp(begin_date),
|
||||
'end_date': self._to_timestamp(end_date)
|
||||
}
|
||||
res = self._post(
|
||||
'shakearound/statistics/device',
|
||||
data=data,
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_page_statistics(self, page_id, begin_date, end_date):
|
||||
"""
|
||||
以页面为维度的数据统计接口
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/8a24bcacad40fe7ee98d1573cb8a6764.html
|
||||
|
||||
:param page_id: 页面 ID
|
||||
:param begin_date: 起始时间,最长时间跨度为30天
|
||||
:param end_date: 结束时间,最长时间跨度为30天
|
||||
:return: 统计数据
|
||||
"""
|
||||
res = self._post(
|
||||
'shakearound/statistics/page',
|
||||
data={
|
||||
'page_id': page_id,
|
||||
'begin_date': self._to_timestamp(begin_date),
|
||||
'end_date': self._to_timestamp(end_date),
|
||||
},
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_apply_status(self, apply_id):
|
||||
"""
|
||||
查询设备ID申请审核状态
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/b9e012f917e3484b7ed02771156411f3.html
|
||||
|
||||
:param apply_id: 批次ID,申请设备ID时所返回的批次ID
|
||||
:return: 批次状态信息
|
||||
"""
|
||||
res = self._post(
|
||||
'shakearound/device/applystatus',
|
||||
data={
|
||||
'apply_id': apply_id,
|
||||
},
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
@@ -1,45 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatTemplate(BaseWeChatAPI):
|
||||
|
||||
def set_industry(self, industry_id1, industry_id2):
|
||||
"""
|
||||
设置所属行业
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/17/304c1885ea66dbedf7dc170d84999a9d.html
|
||||
|
||||
:param industry_id1: 公众号模板消息所属行业编号
|
||||
:param industry_id2: 公众号模板消息所属行业编号
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'template/api_set_industry',
|
||||
data={
|
||||
'industry_id1': industry_id1,
|
||||
'industry_id2': industry_id2
|
||||
}
|
||||
)
|
||||
|
||||
def get(self, template_id_short):
|
||||
"""
|
||||
获得模板ID
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/17/304c1885ea66dbedf7dc170d84999a9d.html
|
||||
|
||||
:param template_id_short: 模板库中模板的编号,有“TM**”和“OPENTMTM**”等形式
|
||||
:return: 模板 ID
|
||||
"""
|
||||
res = self._post(
|
||||
'template/api_add_template',
|
||||
data={
|
||||
'template_id_short': template_id_short
|
||||
},
|
||||
result_processor=lambda x: x['template_id']
|
||||
)
|
||||
return res
|
||||
|
||||
add = get
|
||||
@@ -1,143 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import six
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatUser(BaseWeChatAPI):
|
||||
|
||||
def get(self, user_id, lang='zh_CN'):
|
||||
"""
|
||||
获取用户基本信息
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/14/bb5031008f1494a59c6f71fa0f319c66.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param lang: 返回国家地区语言版本,zh_CN 简体,zh_TW 繁体,en 英语
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
user = client.user.get('openid')
|
||||
|
||||
"""
|
||||
assert lang in ('zh_CN', 'zh_TW', 'en'), 'lang can only be one of \
|
||||
zh_CN, zh_TW, en language codes'
|
||||
return self._get(
|
||||
'user/info',
|
||||
params={
|
||||
'openid': user_id,
|
||||
'lang': lang
|
||||
}
|
||||
)
|
||||
|
||||
def get_followers(self, first_user_id=None):
|
||||
"""
|
||||
获取关注者列表
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/3/17e6919a39c1c53555185907acf70093.html
|
||||
|
||||
:param first_user_id: 可选。第一个拉取的 OPENID,不填默认从头开始拉取
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
followers = client.user.get_followers()
|
||||
|
||||
"""
|
||||
params = {}
|
||||
if first_user_id:
|
||||
params['next_openid'] = first_user_id
|
||||
return self._get(
|
||||
'user/get',
|
||||
params=params
|
||||
)
|
||||
|
||||
def update_remark(self, user_id, remark):
|
||||
"""
|
||||
设置用户备注名
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/10/bf8f4e3074e1cf91eb6518b6d08d223e.html
|
||||
|
||||
:param user_id: 用户 ID 。 就是你收到的 `Message` 的 source
|
||||
:param remark: 备注名
|
||||
:return: 返回的 JSON 数据包
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
client.user.update_remark('openid', 'Remark')
|
||||
|
||||
"""
|
||||
return self._post(
|
||||
'user/info/updateremark',
|
||||
data={
|
||||
'openid': user_id,
|
||||
'remark': remark
|
||||
}
|
||||
)
|
||||
|
||||
def get_group_id(self, user_id):
|
||||
"""
|
||||
获取用户所在分组 ID
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/0/56d992c605a97245eb7e617854b169fc.html
|
||||
|
||||
:param user_id: 用户 ID
|
||||
:return: 用户所在分组 ID
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
group_id = client.user.get_group_id('openid')
|
||||
|
||||
"""
|
||||
res = self._post(
|
||||
'groups/getid',
|
||||
data={'openid': user_id},
|
||||
result_processor=lambda x: x['groupid']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_batch(self, user_list):
|
||||
"""
|
||||
批量获取用户基本信息
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/14/bb5031008f1494a59c6f71fa0f319c66.html#.E6.89.B9.E9.87.8F.E8.8E.B7.E5.8F.96.E7.94.A8.E6.88.B7.E5.9F.BA.E6.9C.AC.E4.BF.A1.E6.81.AF
|
||||
|
||||
:param user_id: user_list
|
||||
:return: 用户信息的 list
|
||||
|
||||
使用示例::
|
||||
|
||||
from wechatpy import WeChatClient
|
||||
|
||||
client = WeChatClient('appid', 'secret')
|
||||
users = client.user.get_batch(['openid1', 'openid2'])
|
||||
users = client.user.get_batch([
|
||||
{'openid': 'openid1', 'lang': 'zh-CN'},
|
||||
{'openid': 'openid2', 'lang': 'en'},
|
||||
])
|
||||
|
||||
"""
|
||||
if all((isinstance(x, six.string_types) for x in user_list)):
|
||||
user_list = [{'openid': oid} for oid in user_list]
|
||||
res = self._post(
|
||||
'user/info/batchget',
|
||||
data={'user_list': user_list},
|
||||
result_processor=lambda x: x['user_info_list']
|
||||
)
|
||||
return res
|
||||
@@ -1,195 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from datetime import datetime, date
|
||||
|
||||
from optionaldict import optionaldict
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatWiFi(BaseWeChatAPI):
|
||||
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/bizwifi/'
|
||||
|
||||
def list_shops(self, page_index=1, page_size=20):
|
||||
"""
|
||||
获取门店列表
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/bcfb5d4578ea818b89913472cf2bbf8f.html
|
||||
|
||||
:param page_index: 可选,分页下标,默认从1开始
|
||||
:param page_size: 可选,每页的个数,默认20个,最大20个
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
res = self._post(
|
||||
'shop/list',
|
||||
data={
|
||||
'pageindex': page_index,
|
||||
'pagesize': page_size,
|
||||
},
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def get_shop(self, shop_id=0):
|
||||
"""
|
||||
查询门店的WiFi信息
|
||||
http://mp.weixin.qq.com/wiki/15/bcfb5d4578ea818b89913472cf2bbf8f.html
|
||||
|
||||
:param shop_id: 门店 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
res = self._post(
|
||||
'shop/get',
|
||||
data={
|
||||
'shop_id': shop_id,
|
||||
},
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def add_device(self, shop_id, ssid, password, bssid):
|
||||
"""
|
||||
添加设备
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/10/6232005bdc497f7cf8e19d4e843c70d2.html
|
||||
|
||||
:param shop_id: 门店 ID
|
||||
:param ssid: 无线网络设备的ssid。非认证公众号添加的ssid必需是“WX”开头(“WX”为大写字母),
|
||||
认证公众号和第三方平台无此限制;所有ssid均不能包含中文字符
|
||||
:param password: 无线网络设备的密码,大于8个字符,不能包含中文字符
|
||||
:param bssid: 无线网络设备无线mac地址,格式冒号分隔,字符长度17个,并且字母小写
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'device/add',
|
||||
data={
|
||||
'shop_id': shop_id,
|
||||
'ssid': ssid,
|
||||
'password': password,
|
||||
'bssid': bssid,
|
||||
}
|
||||
)
|
||||
|
||||
def list_devices(self, shop_id=None, page_index=1, page_size=20):
|
||||
"""
|
||||
查询设备
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/10/6232005bdc497f7cf8e19d4e843c70d2.html
|
||||
|
||||
:param shop_id: 可选,门店 ID
|
||||
:param page_index: 可选,分页下标,默认从1开始
|
||||
:param page_size: 可选,每页的个数,默认20个,最大20个
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict(
|
||||
shop_id=shop_id,
|
||||
pageindex=page_index,
|
||||
pagesize=page_size
|
||||
)
|
||||
res = self._post(
|
||||
'device/list',
|
||||
data=data,
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def delete_device(self, bssid):
|
||||
"""
|
||||
删除设备
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/10/6232005bdc497f7cf8e19d4e843c70d2.html
|
||||
|
||||
:param bssid: 无线网络设备无线mac地址,格式冒号分隔,字符长度17个,并且字母小写
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post('device/delete', data={'bssid': bssid})
|
||||
|
||||
def get_qrcode_url(self, shop_id, img_id):
|
||||
"""
|
||||
获取物料二维码图片网址
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/7/fcd0378ef00617fc276be2b3baa80973.html
|
||||
|
||||
:param shop_id: 门店 ID
|
||||
:param img_id: 物料样式编号:0-二维码,可用于自由设计宣传材料;
|
||||
1-桌贴(二维码),100mm×100mm(宽×高),可直接张贴
|
||||
:return: 二维码图片网址
|
||||
"""
|
||||
res = self._post(
|
||||
'qrcode/get',
|
||||
data={
|
||||
'shop_id': shop_id,
|
||||
'img_id': img_id,
|
||||
},
|
||||
result_processor=lambda x: x['data']['qrcode_url']
|
||||
)
|
||||
return res
|
||||
|
||||
def set_homepage(self, shop_id, template_id, url=None):
|
||||
"""
|
||||
设置商家主页
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/6/2732f3cf83947e0e4971aa8797ee9d6a.html
|
||||
|
||||
:param shop_id: 门店 ID
|
||||
:param template_id: 模板ID,0-默认模板,1-自定义url
|
||||
:param url: 自定义链接,当template_id为1时必填
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = {
|
||||
'shop_id': shop_id,
|
||||
'template_id': template_id,
|
||||
}
|
||||
if url:
|
||||
data['struct'] = {'url': url}
|
||||
return self._post('homepage/set', data=data)
|
||||
|
||||
def get_homepage(self, shop_id):
|
||||
"""
|
||||
查询商家主页
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/6/2732f3cf83947e0e4971aa8797ee9d6a.html
|
||||
|
||||
:param shop_id: 门店 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
res = self._post(
|
||||
'homepage/get',
|
||||
data={'shop_id': shop_id},
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
|
||||
def list_statistics(self, begin_date, end_date, shop_id=-1):
|
||||
"""
|
||||
Wi-Fi数据统计
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/8/dfa2b756b66fca5d9b1211bc18812698.html
|
||||
|
||||
:param begin_date: 起始日期时间,最长时间跨度为30天
|
||||
:param end_date: 结束日期时间戳,最长时间跨度为30天
|
||||
:param shop_id: 可选,门店 ID,按门店ID搜索,-1为总统计
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
if isinstance(begin_date, (datetime, date)):
|
||||
begin_date = begin_date.strftime('%Y-%m-%d')
|
||||
if isinstance(end_date, (datetime, date)):
|
||||
end_date = end_date.strftime('%Y-%m-%d')
|
||||
res = self._post(
|
||||
'statistics/list',
|
||||
data={
|
||||
'begin_date': begin_date,
|
||||
'end_date': end_date,
|
||||
'shop_id': shop_id
|
||||
},
|
||||
result_processor=lambda x: x['data']
|
||||
)
|
||||
return res
|
||||
@@ -1,251 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import sys
|
||||
import time
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
import six
|
||||
import requests
|
||||
from wechatpy.utils import json, get_querystring
|
||||
from wechatpy.session.memorystorage import MemoryStorage
|
||||
from wechatpy.exceptions import WeChatClientException, APILimitedException
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_api_endpoint(obj):
|
||||
return isinstance(obj, BaseWeChatAPI)
|
||||
|
||||
|
||||
class BaseWeChatClient(object):
|
||||
|
||||
API_BASE_URL = ''
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
self = super(BaseWeChatClient, cls).__new__(cls)
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
# Python 2.6 inspect.gemembers bug workaround
|
||||
# http://bugs.python.org/issue1785
|
||||
for _class in cls.__mro__:
|
||||
if issubclass(_class, BaseWeChatClient):
|
||||
for name, api in _class.__dict__.items():
|
||||
if isinstance(api, BaseWeChatAPI):
|
||||
api_cls = type(api)
|
||||
api = api_cls(self)
|
||||
setattr(self, name, api)
|
||||
else:
|
||||
api_endpoints = inspect.getmembers(self, _is_api_endpoint)
|
||||
for name, api in api_endpoints:
|
||||
api_cls = type(api)
|
||||
api = api_cls(self)
|
||||
setattr(self, name, api)
|
||||
return self
|
||||
|
||||
def __init__(self, appid, access_token=None, session=None, timeout=None, auto_retry=True):
|
||||
self.appid = appid
|
||||
self.expires_at = None
|
||||
self.session = session or MemoryStorage()
|
||||
self.timeout = timeout
|
||||
self.auto_retry = auto_retry
|
||||
|
||||
if isinstance(session, six.string_types):
|
||||
from shove import Shove
|
||||
from wechatpy.session.shovestorage import ShoveStorage
|
||||
|
||||
querystring = get_querystring(session)
|
||||
prefix = querystring.get('prefix', ['wechatpy'])[0]
|
||||
|
||||
shove = Shove(session)
|
||||
storage = ShoveStorage(shove, prefix)
|
||||
self.session = storage
|
||||
|
||||
if access_token:
|
||||
self.session.set(self.access_token_key, access_token)
|
||||
|
||||
@property
|
||||
def access_token_key(self):
|
||||
return '{0}_access_token'.format(self.appid)
|
||||
|
||||
def _request(self, method, url_or_endpoint, **kwargs):
|
||||
if not url_or_endpoint.startswith(('http://', 'https://')):
|
||||
api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
|
||||
url = '{base}{endpoint}'.format(
|
||||
base=api_base_url,
|
||||
endpoint=url_or_endpoint
|
||||
)
|
||||
else:
|
||||
url = url_or_endpoint
|
||||
|
||||
# 群发消息上传视频接口地址 HTTPS 证书错误,暂时忽略证书验证
|
||||
if url.startswith('https://file.api.weixin.qq.com'):
|
||||
kwargs['verify'] = False
|
||||
|
||||
if 'params' not in kwargs:
|
||||
kwargs['params'] = {}
|
||||
if isinstance(kwargs['params'], dict) and \
|
||||
'access_token' not in kwargs['params']:
|
||||
kwargs['params']['access_token'] = self.access_token
|
||||
if isinstance(kwargs.get('data', ''), dict):
|
||||
body = json.dumps(kwargs['data'], ensure_ascii=False)
|
||||
body = body.encode('utf-8')
|
||||
kwargs['data'] = body
|
||||
|
||||
kwargs['timeout'] = kwargs.get('timeout', self.timeout)
|
||||
result_processor = kwargs.pop('result_processor', None)
|
||||
res = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
**kwargs
|
||||
)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except requests.RequestException as reqe:
|
||||
raise WeChatClientException(
|
||||
errcode=None,
|
||||
errmsg=None,
|
||||
client=self,
|
||||
request=reqe.request,
|
||||
response=reqe.response
|
||||
)
|
||||
|
||||
return self._handle_result(
|
||||
res, method, url, result_processor, **kwargs
|
||||
)
|
||||
|
||||
def _decode_result(self, res):
|
||||
try:
|
||||
result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
|
||||
except (TypeError, ValueError):
|
||||
# Return origin response object if we can not decode it as JSON
|
||||
logger.debug('Can not decode response as JSON', exc_info=True)
|
||||
return res
|
||||
return result
|
||||
|
||||
def _handle_result(self, res, method=None, url=None,
|
||||
result_processor=None, **kwargs):
|
||||
if not isinstance(res, dict):
|
||||
# Dirty hack around asyncio based AsyncWeChatClient
|
||||
result = self._decode_result(res)
|
||||
else:
|
||||
result = res
|
||||
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
|
||||
if 'base_resp' in result:
|
||||
# Different response in device APIs. Fuck tencent!
|
||||
result = result['base_resp']
|
||||
if 'errcode' in result:
|
||||
result['errcode'] = int(result['errcode'])
|
||||
|
||||
if 'errcode' in result and result['errcode'] != 0:
|
||||
errcode = result['errcode']
|
||||
errmsg = result.get('errmsg', errcode)
|
||||
if errcode in (40001, 40014, 42001) and self.auto_retry:
|
||||
logger.info('Access token expired, fetch a new one and retry request')
|
||||
self.fetch_access_token()
|
||||
access_token = self.session.get(self.access_token_key)
|
||||
kwargs['params']['access_token'] = access_token
|
||||
return self._request(
|
||||
method=method,
|
||||
url_or_endpoint=url,
|
||||
result_processor=result_processor,
|
||||
**kwargs
|
||||
)
|
||||
elif errcode == 45009:
|
||||
# api freq out of limit
|
||||
raise APILimitedException(
|
||||
errcode,
|
||||
errmsg,
|
||||
client=self,
|
||||
request=res.request,
|
||||
response=res
|
||||
)
|
||||
else:
|
||||
raise WeChatClientException(
|
||||
errcode,
|
||||
errmsg,
|
||||
client=self,
|
||||
request=res.request,
|
||||
response=res
|
||||
)
|
||||
|
||||
return result if not result_processor else result_processor(result)
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._request(
|
||||
method='get',
|
||||
url_or_endpoint=url,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
_get = get
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._request(
|
||||
method='post',
|
||||
url_or_endpoint=url,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
_post = post
|
||||
|
||||
def _fetch_access_token(self, url, params):
|
||||
""" The real fetch access token """
|
||||
logger.info('Fetching access token')
|
||||
res = requests.get(
|
||||
url=url,
|
||||
params=params
|
||||
)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except requests.RequestException as reqe:
|
||||
raise WeChatClientException(
|
||||
errcode=None,
|
||||
errmsg=None,
|
||||
client=self,
|
||||
request=reqe.request,
|
||||
response=reqe.response
|
||||
)
|
||||
result = res.json()
|
||||
if 'errcode' in result and result['errcode'] != 0:
|
||||
raise WeChatClientException(
|
||||
result['errcode'],
|
||||
result['errmsg'],
|
||||
client=self,
|
||||
request=res.request,
|
||||
response=res
|
||||
)
|
||||
|
||||
expires_in = 7200
|
||||
if 'expires_in' in result:
|
||||
expires_in = result['expires_in']
|
||||
self.session.set(
|
||||
self.access_token_key,
|
||||
result['access_token'],
|
||||
expires_in
|
||||
)
|
||||
self.expires_at = int(time.time()) + expires_in
|
||||
return result
|
||||
|
||||
def fetch_access_token(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
""" WeChat access token """
|
||||
access_token = self.session.get(self.access_token_key)
|
||||
if access_token:
|
||||
if not self.expires_at:
|
||||
# user provided access_token, just return it
|
||||
return access_token
|
||||
|
||||
timestamp = time.time()
|
||||
if self.expires_at - timestamp > 60:
|
||||
return access_token
|
||||
|
||||
self.fetch_access_token()
|
||||
return self.session.get(self.access_token_key)
|
||||
@@ -1,431 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.component
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides client library for WeChat Open Platform
|
||||
|
||||
:copyright: (c) 2015 by hunter007.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
import logging
|
||||
|
||||
import six
|
||||
import requests
|
||||
import xmltodict
|
||||
|
||||
from wechatpy.utils import to_text, to_binary, get_querystring, json
|
||||
from wechatpy.fields import StringField, DateTimeField
|
||||
from wechatpy.messages import MessageMetaClass
|
||||
from wechatpy.session.memorystorage import MemoryStorage
|
||||
from wechatpy.exceptions import WeChatClientException, APILimitedException
|
||||
from wechatpy.crypto import WeChatCrypto
|
||||
from wechatpy.client import WeChatComponentClient
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseComponentMessage(six.with_metaclass(MessageMetaClass)):
|
||||
"""Base class for all component messages and events"""
|
||||
type = 'unknown'
|
||||
appid = StringField('AppId')
|
||||
create_time = DateTimeField('CreateTime')
|
||||
|
||||
def __init__(self, message):
|
||||
self._data = message
|
||||
|
||||
def __repr__(self):
|
||||
_repr = "{klass}({msg})".format(
|
||||
klass=self.__class__.__name__,
|
||||
msg=repr(self._data)
|
||||
)
|
||||
if six.PY2:
|
||||
return to_binary(_repr)
|
||||
else:
|
||||
return to_text(_repr)
|
||||
|
||||
|
||||
class ComponentVerifyTicketMessage(BaseComponentMessage):
|
||||
"""
|
||||
component_verify_ticket协议
|
||||
"""
|
||||
type = 'component_verify_ticket'
|
||||
verify_ticket = StringField('ComponentVerifyTicket')
|
||||
|
||||
|
||||
class ComponentUnauthorizedMessage(BaseComponentMessage):
|
||||
"""
|
||||
取消授权通知
|
||||
"""
|
||||
type = 'unauthorized'
|
||||
authorizer_appid = StringField('AuthorizerAppid')
|
||||
|
||||
|
||||
class BaseWeChatComponent(object):
|
||||
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/cgi-bin'
|
||||
|
||||
def __init__(self,
|
||||
component_appid,
|
||||
component_appsecret,
|
||||
component_token,
|
||||
encoding_aes_key,
|
||||
session=None):
|
||||
"""
|
||||
:param component_appid: 第三方平台appid
|
||||
:param component_appsecret: 第三方平台appsecret
|
||||
:param component_token: 公众号消息校验Token
|
||||
:param encoding_aes_key: 公众号消息加解密Key
|
||||
"""
|
||||
self.component_appid = component_appid
|
||||
self.component_appsecret = component_appsecret
|
||||
self.expires_at = None
|
||||
self.crypto = WeChatCrypto(
|
||||
component_token, encoding_aes_key, component_appid)
|
||||
self.session = session or MemoryStorage()
|
||||
|
||||
if isinstance(session, six.string_types):
|
||||
from shove import Shove
|
||||
from wechatpy.session.shovestorage import ShoveStorage
|
||||
|
||||
querystring = get_querystring(session)
|
||||
prefix = querystring.get('prefix', ['wechatpy'])[0]
|
||||
|
||||
shove = Shove(session)
|
||||
storage = ShoveStorage(shove, prefix)
|
||||
self.session = storage
|
||||
|
||||
@property
|
||||
def component_verify_ticket(self):
|
||||
return self.session.get('component_verify_ticket')
|
||||
|
||||
def _request(self, method, url_or_endpoint, **kwargs):
|
||||
if not url_or_endpoint.startswith(('http://', 'https://')):
|
||||
api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
|
||||
url = '{base}{endpoint}'.format(
|
||||
base=api_base_url,
|
||||
endpoint=url_or_endpoint
|
||||
)
|
||||
else:
|
||||
url = url_or_endpoint
|
||||
|
||||
if 'params' not in kwargs:
|
||||
kwargs['params'] = {}
|
||||
if isinstance(kwargs['params'], dict) and \
|
||||
'component_access_token' not in kwargs['params']:
|
||||
kwargs['params'][
|
||||
'component_access_token'] = self.access_token
|
||||
if isinstance(kwargs['data'], dict):
|
||||
kwargs['data'] = json.dumps(kwargs['data'])
|
||||
|
||||
res = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
**kwargs
|
||||
)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except requests.RequestException as reqe:
|
||||
raise WeChatClientException(
|
||||
errcode=None,
|
||||
errmsg=None,
|
||||
client=self,
|
||||
request=reqe.request,
|
||||
response=reqe.response
|
||||
)
|
||||
|
||||
return self._handle_result(res, method, url, **kwargs)
|
||||
|
||||
def _handle_result(self, res, method=None, url=None, **kwargs):
|
||||
result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
|
||||
if 'errcode' in result:
|
||||
result['errcode'] = int(result['errcode'])
|
||||
|
||||
if 'errcode' in result and result['errcode'] != 0:
|
||||
errcode = result['errcode']
|
||||
errmsg = result['errmsg']
|
||||
if errcode == 42001:
|
||||
logger.info('Component access token expired, fetch a new one and retry request')
|
||||
self.fetch_component_access_token()
|
||||
kwargs['params']['component_access_token'] = self.session.get(
|
||||
'component_access_token'
|
||||
)
|
||||
return self._request(
|
||||
method=method,
|
||||
url_or_endpoint=url,
|
||||
**kwargs
|
||||
)
|
||||
elif errcode == 45009:
|
||||
# api freq out of limit
|
||||
raise APILimitedException(
|
||||
errcode,
|
||||
errmsg,
|
||||
client=self,
|
||||
request=res.request,
|
||||
response=res
|
||||
)
|
||||
else:
|
||||
raise WeChatClientException(
|
||||
errcode,
|
||||
errmsg,
|
||||
client=self,
|
||||
request=res.request,
|
||||
response=res
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def fetch_access_token(self):
|
||||
"""
|
||||
获取 component_access_token
|
||||
详情请参考 https://open.weixin.qq.com/cgi-bin/showdocument?action=dir_list\
|
||||
&t=resource/res_list&verify=1&id=open1419318587&token=&lang=zh_CN
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
url = '{0}{1}'.format(
|
||||
self.API_BASE_URL,
|
||||
'/component/api_component_token'
|
||||
)
|
||||
return self._fetch_access_token(
|
||||
url=url,
|
||||
data=json.dumps({
|
||||
'component_appid': self.component_appid,
|
||||
'component_appsecret': self.component_appsecret,
|
||||
'component_verify_ticket': self.component_verify_ticket
|
||||
})
|
||||
)
|
||||
|
||||
def _fetch_access_token(self, url, data):
|
||||
""" The real fetch access token """
|
||||
logger.info('Fetching component access token')
|
||||
res = requests.post(
|
||||
url=url,
|
||||
data=data
|
||||
)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except requests.RequestException as reqe:
|
||||
raise WeChatClientException(
|
||||
errcode=None,
|
||||
errmsg=None,
|
||||
client=self,
|
||||
request=reqe.request,
|
||||
response=reqe.response
|
||||
)
|
||||
result = res.json()
|
||||
if 'errcode' in result and result['errcode'] != 0:
|
||||
raise WeChatClientException(
|
||||
result['errcode'],
|
||||
result['errmsg'],
|
||||
client=self,
|
||||
request=res.request,
|
||||
response=res
|
||||
)
|
||||
|
||||
expires_in = 7200
|
||||
if 'expires_in' in result:
|
||||
expires_in = result['expires_in']
|
||||
self.session.set(
|
||||
'component_access_token',
|
||||
result['component_access_token'],
|
||||
expires_in
|
||||
)
|
||||
self.expires_at = int(time.time()) + expires_in
|
||||
return result
|
||||
|
||||
@property
|
||||
def access_token(self):
|
||||
""" WeChat component access token """
|
||||
access_token = self.session.get('component_access_token')
|
||||
if access_token:
|
||||
if not self.expires_at:
|
||||
# user provided access_token, just return it
|
||||
return access_token
|
||||
|
||||
timestamp = time.time()
|
||||
if self.expires_at - timestamp > 60:
|
||||
return access_token
|
||||
|
||||
self.fetch_access_token()
|
||||
return self.session.get('component_access_token')
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
return self._request(
|
||||
method='get',
|
||||
url_or_endpoint=url,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def post(self, url, **kwargs):
|
||||
return self._request(
|
||||
method='post',
|
||||
url_or_endpoint=url,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
|
||||
class WeChatComponent(BaseWeChatComponent):
|
||||
|
||||
def create_preauthcode(self):
|
||||
"""
|
||||
获取预授权码
|
||||
"""
|
||||
return self.post(
|
||||
'/component/api_create_preauthcode',
|
||||
data={
|
||||
'component_appid': self.component_appid
|
||||
}
|
||||
)
|
||||
|
||||
def query_auth(self, authorization_code):
|
||||
"""
|
||||
使用授权码换取公众号的授权信息
|
||||
|
||||
:params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明
|
||||
"""
|
||||
return self.post(
|
||||
'/component/api_query_auth',
|
||||
data={
|
||||
'component_appid': self.component_appid,
|
||||
'authorization_code': authorization_code
|
||||
}
|
||||
)
|
||||
|
||||
def refresh_authorizer_token(
|
||||
self, authorizer_appid, authorizer_refresh_token):
|
||||
"""
|
||||
获取(刷新)授权公众号的令牌
|
||||
|
||||
:params authorizer_appid: 授权方appid
|
||||
:params authorizer_refresh_token: 授权方的刷新令牌
|
||||
"""
|
||||
return self.post(
|
||||
'/component/api_authorizer_token',
|
||||
data={
|
||||
'component_appid': self.component_appid,
|
||||
'authorizer_appid': authorizer_appid,
|
||||
'authorizer_refresh_token': authorizer_refresh_token
|
||||
}
|
||||
)
|
||||
|
||||
def get_authorizer_info(self, authorizer_appid):
|
||||
"""
|
||||
获取授权方的账户信息
|
||||
|
||||
:params authorizer_appid: 授权方appid
|
||||
"""
|
||||
return self.post(
|
||||
'/component/api_get_authorizer_info',
|
||||
data={
|
||||
'component_appid': self.component_appid,
|
||||
'authorizer_appid': authorizer_appid,
|
||||
}
|
||||
)
|
||||
|
||||
def get_authorizer_option(self, authorizer_appid, option_name):
|
||||
"""
|
||||
获取授权方的选项设置信息
|
||||
|
||||
:params authorizer_appid: 授权公众号appid
|
||||
:params option_name: 选项名称
|
||||
"""
|
||||
return self.post(
|
||||
'/component/api_get_authorizer_option',
|
||||
data={
|
||||
'component_appid': self.component_appid,
|
||||
'authorizer_appid': authorizer_appid,
|
||||
'option_name': option_name
|
||||
}
|
||||
)
|
||||
|
||||
def set_authorizer_option(
|
||||
self, authorizer_appid, option_name, option_value):
|
||||
"""
|
||||
设置授权方的选项信息
|
||||
|
||||
:params authorizer_appid: 授权公众号appid
|
||||
:params option_name: 选项名称
|
||||
:params option_value: 设置的选项值
|
||||
"""
|
||||
return self.post(
|
||||
'/component/api_set_authorizer_option',
|
||||
data={
|
||||
'component_appid': self.component_appid,
|
||||
'authorizer_appid': authorizer_appid,
|
||||
'option_name': option_name,
|
||||
'option_value': option_value
|
||||
}
|
||||
)
|
||||
|
||||
def get_client_by_authorization_code(self, authorization_code):
|
||||
"""
|
||||
通过授权码直接获取 Client 对象
|
||||
|
||||
:params authorization_code: 授权code,会在授权成功时返回给第三方平台,详见第三方平台授权流程说明
|
||||
"""
|
||||
result = self.query_auth(authorization_code)
|
||||
access_token = result['authorization_info']['authorizer_access_token']
|
||||
refresh_token = result['authorization_info']['authorizer_refresh_token'] # NOQA
|
||||
authorizer_appid = result['authorization_info']['authorizer_appid'] # noqa
|
||||
return WeChatComponentClient(
|
||||
authorizer_appid, self, access_token, refresh_token,
|
||||
session=self.session
|
||||
)
|
||||
|
||||
def get_client_by_appid(self, authorizer_appid):
|
||||
"""
|
||||
通过 authorizer_appid 获取 Client 对象
|
||||
|
||||
:params authorizer_appid: 授权公众号appid
|
||||
"""
|
||||
access_token_key = '{0}_access_token'.format(authorizer_appid)
|
||||
refresh_token_key = '{0}_refresh_token'.format(authorizer_appid)
|
||||
access_token = self.session.get(access_token_key)
|
||||
refresh_token = self.session.get(refresh_token_key)
|
||||
|
||||
if not access_token:
|
||||
ret = self.refresh_authorizer_token(
|
||||
authorizer_appid,
|
||||
refresh_token
|
||||
)
|
||||
access_token = ret['authorizer_access_token']
|
||||
refresh_token = ret['authorizer_refresh_token']
|
||||
|
||||
return WeChatComponentClient(
|
||||
authorizer_appid,
|
||||
self,
|
||||
access_token,
|
||||
refresh_token,
|
||||
session=self.session
|
||||
)
|
||||
|
||||
def cache_component_verify_ticket(self, msg, signature, timestamp, nonce):
|
||||
"""
|
||||
处理 wechat server 推送的 component_verify_ticket消息
|
||||
|
||||
:params msg: 加密内容
|
||||
:params signature: 消息签名
|
||||
:params timestamp: 时间戳
|
||||
:params nonce: 随机数
|
||||
"""
|
||||
content = self.crypto.decrypt_message(msg, signature, timestamp, nonce)
|
||||
message = xmltodict.parse(to_text(content))['xml']
|
||||
o = ComponentVerifyTicketMessage(message)
|
||||
self.session.set(o.type, o.verify_ticket, 600)
|
||||
|
||||
def get_unauthorized(self, msg, signature, timestamp, nonce):
|
||||
"""
|
||||
处理取消授权通知
|
||||
|
||||
:params msg: 加密内容
|
||||
:params signature: 消息签名
|
||||
:params timestamp: 时间戳
|
||||
:params nonce: 随机数
|
||||
"""
|
||||
content = self.crypto.decrypt_message(msg, signature, timestamp, nonce)
|
||||
message = xmltodict.parse(to_text(content))['xml']
|
||||
return ComponentUnauthorizedMessage(message)
|
||||
@@ -1,120 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.crypto
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides some crypto tools for WeChat and WeChat enterprise
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
import base64
|
||||
|
||||
from wechatpy.utils import to_text, to_binary, WeChatSigner
|
||||
from wechatpy.exceptions import (
|
||||
InvalidAppIdException,
|
||||
InvalidSignatureException
|
||||
)
|
||||
from wechatpy.crypto.base import BasePrpCrypto
|
||||
|
||||
|
||||
def _get_signature(token, timestamp, nonce, encrypt):
|
||||
signer = WeChatSigner()
|
||||
signer.add_data(token, timestamp, nonce, encrypt)
|
||||
return signer.signature
|
||||
|
||||
|
||||
class PrpCrypto(BasePrpCrypto):
|
||||
|
||||
def encrypt(self, text, app_id):
|
||||
return self._encrypt(text, app_id)
|
||||
|
||||
def decrypt(self, text, app_id):
|
||||
return self._decrypt(text, app_id, InvalidAppIdException)
|
||||
|
||||
|
||||
class BaseWeChatCrypto(object):
|
||||
|
||||
def __init__(self, token, encoding_aes_key, _id):
|
||||
encoding_aes_key = to_binary(encoding_aes_key + '=')
|
||||
self.key = base64.b64decode(encoding_aes_key)
|
||||
assert len(self.key) == 32
|
||||
self.token = token
|
||||
self._id = _id
|
||||
|
||||
def _check_signature(self,
|
||||
signature,
|
||||
timestamp,
|
||||
nonce,
|
||||
echo_str,
|
||||
crypto_class=None):
|
||||
_signature = _get_signature(self.token, timestamp, nonce, echo_str)
|
||||
if _signature != signature:
|
||||
raise InvalidSignatureException()
|
||||
pc = crypto_class(self.key)
|
||||
return pc.decrypt(echo_str, self._id)
|
||||
|
||||
def _encrypt_message(self,
|
||||
msg,
|
||||
nonce,
|
||||
timestamp=None,
|
||||
crypto_class=None):
|
||||
from wechatpy.replies import BaseReply
|
||||
|
||||
xml = """<xml>
|
||||
<Encrypt><![CDATA[{encrypt}]]></Encrypt>
|
||||
<MsgSignature><![CDATA[{signature}]]></MsgSignature>
|
||||
<TimeStamp>{timestamp}</TimeStamp>
|
||||
<Nonce><![CDATA[{nonce}]]></Nonce>
|
||||
</xml>"""
|
||||
if isinstance(msg, BaseReply):
|
||||
msg = msg.render()
|
||||
timestamp = timestamp or to_binary(int(time.time()))
|
||||
pc = crypto_class(self.key)
|
||||
encrypt = to_text(pc.encrypt(msg, self._id))
|
||||
signature = _get_signature(self.token, timestamp, nonce, encrypt)
|
||||
return to_text(xml.format(
|
||||
encrypt=encrypt,
|
||||
signature=signature,
|
||||
timestamp=timestamp,
|
||||
nonce=nonce
|
||||
))
|
||||
|
||||
def _decrypt_message(self,
|
||||
msg,
|
||||
signature,
|
||||
timestamp,
|
||||
nonce,
|
||||
crypto_class=None):
|
||||
if not isinstance(msg, dict):
|
||||
import xmltodict
|
||||
|
||||
msg = xmltodict.parse(to_text(msg))['xml']
|
||||
|
||||
encrypt = msg['Encrypt']
|
||||
_signature = _get_signature(self.token, timestamp, nonce, encrypt)
|
||||
if _signature != signature:
|
||||
raise InvalidSignatureException()
|
||||
pc = crypto_class(self.key)
|
||||
return pc.decrypt(encrypt, self._id)
|
||||
|
||||
|
||||
class WeChatCrypto(BaseWeChatCrypto):
|
||||
|
||||
def __init__(self, token, encoding_aes_key, app_id):
|
||||
super(WeChatCrypto, self).__init__(token, encoding_aes_key, app_id)
|
||||
self.app_id = app_id
|
||||
|
||||
def encrypt_message(self, msg, nonce, timestamp=None):
|
||||
return self._encrypt_message(msg, nonce, timestamp, PrpCrypto)
|
||||
|
||||
def decrypt_message(self, msg, signature, timestamp, nonce):
|
||||
return self._decrypt_message(
|
||||
msg,
|
||||
signature,
|
||||
timestamp,
|
||||
nonce,
|
||||
PrpCrypto
|
||||
)
|
||||
@@ -1,52 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import struct
|
||||
import socket
|
||||
import base64
|
||||
|
||||
from wechatpy.utils import to_text, to_binary, random_string, byte2int
|
||||
from wechatpy.crypto.pkcs7 import PKCS7Encoder
|
||||
try:
|
||||
from wechatpy.crypto.cryptography import WeChatCipher
|
||||
except ImportError:
|
||||
try:
|
||||
from wechatpy.crypto.pycrypto import WeChatCipher
|
||||
except ImportError:
|
||||
raise Exception('You must install either cryptography or PyCrypto!')
|
||||
|
||||
|
||||
class BasePrpCrypto(object):
|
||||
|
||||
def __init__(self, key):
|
||||
self.cipher = WeChatCipher(key)
|
||||
|
||||
def get_random_string(self):
|
||||
return random_string(16)
|
||||
|
||||
def _encrypt(self, text, _id):
|
||||
text = to_binary(text)
|
||||
tmp_list = []
|
||||
tmp_list.append(to_binary(self.get_random_string()))
|
||||
length = struct.pack(b'I', socket.htonl(len(text)))
|
||||
tmp_list.append(length)
|
||||
tmp_list.append(text)
|
||||
tmp_list.append(to_binary(_id))
|
||||
|
||||
text = b''.join(tmp_list)
|
||||
text = PKCS7Encoder.encode(text)
|
||||
|
||||
ciphertext = to_binary(self.cipher.encrypt(text))
|
||||
return base64.b64encode(ciphertext)
|
||||
|
||||
def _decrypt(self, text, _id, exception=None):
|
||||
text = to_binary(text)
|
||||
plain_text = self.cipher.decrypt(base64.b64decode(text))
|
||||
padding = byte2int(plain_text[-1])
|
||||
content = plain_text[16:-padding]
|
||||
xml_length = socket.ntohl(struct.unpack(b'I', content[:4])[0])
|
||||
xml_content = to_text(content[4:xml_length + 4])
|
||||
from_id = to_text(content[xml_length + 4:])
|
||||
if from_id != _id:
|
||||
exception = exception or Exception
|
||||
raise exception()
|
||||
return xml_content
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
|
||||
class WeChatCipher(object):
|
||||
|
||||
def __init__(self, key):
|
||||
backend = default_backend()
|
||||
self.cipher = Cipher(
|
||||
algorithms.AES(key),
|
||||
modes.CBC(key[:16]),
|
||||
backend=backend
|
||||
)
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
encryptor = self.cipher.encryptor()
|
||||
return encryptor.update(plaintext) + encryptor.finalize()
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
decryptor = self.cipher.decryptor()
|
||||
return decryptor.update(ciphertext) + decryptor.finalize()
|
||||
@@ -1,23 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.utils import to_binary, byte2int
|
||||
|
||||
|
||||
class PKCS7Encoder(object):
|
||||
block_size = 32
|
||||
|
||||
@classmethod
|
||||
def encode(cls, text):
|
||||
length = len(text)
|
||||
padding_count = cls.block_size - length % cls.block_size
|
||||
if padding_count == 0:
|
||||
padding_count = cls.block_size
|
||||
padding = to_binary(chr(padding_count))
|
||||
return text + padding * padding_count
|
||||
|
||||
@classmethod
|
||||
def decode(cls, decrypted):
|
||||
padding = byte2int(decrypted[-1])
|
||||
if padding < 1 or padding > 32:
|
||||
padding = 0
|
||||
return decrypted[:-padding]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user