优化工单模块,增加企微模块
This commit is contained in:
33
sg_wechat_enterprise/we_api/__init__.py
Normal file
33
sg_wechat_enterprise/we_api/__init__.py
Normal file
@@ -0,0 +1,33 @@
|
||||
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())
|
||||
21
sg_wechat_enterprise/we_api/_compat.py
Normal file
21
sg_wechat_enterprise/we_api/_compat.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- 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
|
||||
140
sg_wechat_enterprise/we_api/client/__init__.py
Normal file
140
sg_wechat_enterprise/we_api/client/__init__.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# -*- 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
|
||||
23
sg_wechat_enterprise/we_api/client/api/__init__.py
Normal file
23
sg_wechat_enterprise/we_api/client/api/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- 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
|
||||
29
sg_wechat_enterprise/we_api/client/api/base.py
Normal file
29
sg_wechat_enterprise/we_api/client/api/base.py
Normal file
@@ -0,0 +1,29 @@
|
||||
# -*- 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
|
||||
431
sg_wechat_enterprise/we_api/client/api/card.py
Normal file
431
sg_wechat_enterprise/we_api/client/api/card.py
Normal file
@@ -0,0 +1,431 @@
|
||||
# -*- 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
|
||||
)
|
||||
242
sg_wechat_enterprise/we_api/client/api/customservice.py
Normal file
242
sg_wechat_enterprise/we_api/client/api/customservice.py
Normal file
@@ -0,0 +1,242 @@
|
||||
# -*- 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
|
||||
360
sg_wechat_enterprise/we_api/client/api/datacube.py
Normal file
360
sg_wechat_enterprise/we_api/client/api/datacube.py
Normal file
@@ -0,0 +1,360 @@
|
||||
# -*- 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
|
||||
284
sg_wechat_enterprise/we_api/client/api/device.py
Normal file
284
sg_wechat_enterprise/we_api/client/api/device.py
Normal file
@@ -0,0 +1,284 @@
|
||||
# -*- 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
|
||||
}
|
||||
)
|
||||
148
sg_wechat_enterprise/we_api/client/api/group.py
Normal file
148
sg_wechat_enterprise/we_api/client/api/group.py
Normal file
@@ -0,0 +1,148 @@
|
||||
# -*- 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
|
||||
}
|
||||
}
|
||||
)
|
||||
58
sg_wechat_enterprise/we_api/client/api/jsapi.py
Normal file
58
sg_wechat_enterprise/we_api/client/api/jsapi.py
Normal file
@@ -0,0 +1,58 @@
|
||||
# -*- 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
|
||||
167
sg_wechat_enterprise/we_api/client/api/material.py
Normal file
167
sg_wechat_enterprise/we_api/client/api/material.py
Normal file
@@ -0,0 +1,167 @@
|
||||
# -*- 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')
|
||||
126
sg_wechat_enterprise/we_api/client/api/media.py
Normal file
126
sg_wechat_enterprise/we_api/client/api/media.py
Normal file
@@ -0,0 +1,126 @@
|
||||
# -*- 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
|
||||
235
sg_wechat_enterprise/we_api/client/api/menu.py
Normal file
235
sg_wechat_enterprise/we_api/client/api/menu.py
Normal file
@@ -0,0 +1,235 @@
|
||||
# -*- 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}
|
||||
)
|
||||
72
sg_wechat_enterprise/we_api/client/api/merchant/__init__.py
Normal file
72
sg_wechat_enterprise/we_api/client/api/merchant/__init__.py
Normal file
@@ -0,0 +1,72 @@
|
||||
# -*- 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
|
||||
}
|
||||
)
|
||||
30
sg_wechat_enterprise/we_api/client/api/merchant/category.py
Normal file
30
sg_wechat_enterprise/we_api/client/api/merchant/category.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- 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
|
||||
17
sg_wechat_enterprise/we_api/client/api/merchant/common.py
Normal file
17
sg_wechat_enterprise/we_api/client/api/merchant/common.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# -*- 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
|
||||
48
sg_wechat_enterprise/we_api/client/api/merchant/express.py
Normal file
48
sg_wechat_enterprise/we_api/client/api/merchant/express.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- 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
|
||||
60
sg_wechat_enterprise/we_api/client/api/merchant/group.py
Normal file
60
sg_wechat_enterprise/we_api/client/api/merchant/group.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- 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
|
||||
52
sg_wechat_enterprise/we_api/client/api/merchant/order.py
Normal file
52
sg_wechat_enterprise/we_api/client/api/merchant/order.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- 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
|
||||
}
|
||||
)
|
||||
50
sg_wechat_enterprise/we_api/client/api/merchant/shelf.py
Normal file
50
sg_wechat_enterprise/we_api/client/api/merchant/shelf.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- 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
|
||||
}
|
||||
)
|
||||
26
sg_wechat_enterprise/we_api/client/api/merchant/stock.py
Normal file
26
sg_wechat_enterprise/we_api/client/api/merchant/stock.py
Normal file
@@ -0,0 +1,26 @@
|
||||
# -*- 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
|
||||
}
|
||||
)
|
||||
572
sg_wechat_enterprise/we_api/client/api/message.py
Normal file
572
sg_wechat_enterprise/we_api/client/api/message.py
Normal file
@@ -0,0 +1,572 @@
|
||||
# -*- 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
|
||||
)
|
||||
51
sg_wechat_enterprise/we_api/client/api/misc.py
Normal file
51
sg_wechat_enterprise/we_api/client/api/misc.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# -*- 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
|
||||
97
sg_wechat_enterprise/we_api/client/api/poi.py
Normal file
97
sg_wechat_enterprise/we_api/client/api/poi.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# -*- 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
|
||||
87
sg_wechat_enterprise/we_api/client/api/qrcode.py
Normal file
87
sg_wechat_enterprise/we_api/client/api/qrcode.py
Normal file
@@ -0,0 +1,87 @@
|
||||
# -*- 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)
|
||||
171
sg_wechat_enterprise/we_api/client/api/scan.py
Normal file
171
sg_wechat_enterprise/we_api/client/api/scan.py
Normal file
@@ -0,0 +1,171 @@
|
||||
# -*- 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})
|
||||
59
sg_wechat_enterprise/we_api/client/api/semantic.py
Normal file
59
sg_wechat_enterprise/we_api/client/api/semantic.py
Normal file
@@ -0,0 +1,59 @@
|
||||
# -*- 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
|
||||
)
|
||||
376
sg_wechat_enterprise/we_api/client/api/shakearound.py
Normal file
376
sg_wechat_enterprise/we_api/client/api/shakearound.py
Normal file
@@ -0,0 +1,376 @@
|
||||
# -*- 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
|
||||
45
sg_wechat_enterprise/we_api/client/api/template.py
Normal file
45
sg_wechat_enterprise/we_api/client/api/template.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- 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
|
||||
143
sg_wechat_enterprise/we_api/client/api/user.py
Normal file
143
sg_wechat_enterprise/we_api/client/api/user.py
Normal file
@@ -0,0 +1,143 @@
|
||||
# -*- 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
|
||||
195
sg_wechat_enterprise/we_api/client/api/wifi.py
Normal file
195
sg_wechat_enterprise/we_api/client/api/wifi.py
Normal file
@@ -0,0 +1,195 @@
|
||||
# -*- 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
|
||||
251
sg_wechat_enterprise/we_api/client/base.py
Normal file
251
sg_wechat_enterprise/we_api/client/base.py
Normal file
@@ -0,0 +1,251 @@
|
||||
# -*- 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)
|
||||
431
sg_wechat_enterprise/we_api/component.py
Normal file
431
sg_wechat_enterprise/we_api/component.py
Normal file
@@ -0,0 +1,431 @@
|
||||
# -*- 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)
|
||||
120
sg_wechat_enterprise/we_api/crypto/__init__.py
Normal file
120
sg_wechat_enterprise/we_api/crypto/__init__.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# -*- 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
|
||||
)
|
||||
52
sg_wechat_enterprise/we_api/crypto/base.py
Normal file
52
sg_wechat_enterprise/we_api/crypto/base.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- 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
|
||||
23
sg_wechat_enterprise/we_api/crypto/cryptography.py
Normal file
23
sg_wechat_enterprise/we_api/crypto/cryptography.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- 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()
|
||||
23
sg_wechat_enterprise/we_api/crypto/pkcs7.py
Normal file
23
sg_wechat_enterprise/we_api/crypto/pkcs7.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- 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]
|
||||
15
sg_wechat_enterprise/we_api/crypto/pycrypto.py
Normal file
15
sg_wechat_enterprise/we_api/crypto/pycrypto.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from Crypto.Cipher import AES
|
||||
|
||||
|
||||
class WeChatCipher(object):
|
||||
|
||||
def __init__(self, key):
|
||||
self.cipher = AES.new(key, AES.MODE_CBC, key[:16])
|
||||
|
||||
def encrypt(self, plaintext):
|
||||
return self.cipher.encrypt(plaintext)
|
||||
|
||||
def decrypt(self, ciphertext):
|
||||
return self.cipher.decrypt(ciphertext)
|
||||
7
sg_wechat_enterprise/we_api/enterprise/__init__.py
Normal file
7
sg_wechat_enterprise/we_api/enterprise/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.enterprise.parser import parse_message # NOQA
|
||||
from wechatpy.enterprise.replies import create_reply # NOQA
|
||||
from wechatpy.enterprise.crypto import WeChatCrypto # NOQA
|
||||
from wechatpy.enterprise.client import WeChatClient # NOQA
|
||||
44
sg_wechat_enterprise/we_api/enterprise/client/__init__.py
Normal file
44
sg_wechat_enterprise/we_api/enterprise/client/__init__.py
Normal file
@@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.client.base import BaseWeChatClient
|
||||
from wechatpy.enterprise.client import api
|
||||
|
||||
|
||||
class WeChatClient(BaseWeChatClient):
|
||||
|
||||
API_BASE_URL = 'https://qyapi.weixin.qq.com/cgi-bin/'
|
||||
|
||||
user = api.WeChatUser()
|
||||
department = api.WeChatDepartment()
|
||||
menu = api.WeChatMenu()
|
||||
message = api.WeChatMessage()
|
||||
tag = api.WeChatTag()
|
||||
media = api.WeChatMedia()
|
||||
misc = api.WeChatMisc()
|
||||
agent = api.WeChatAgent()
|
||||
batch = api.WeChatBatch()
|
||||
jsapi = api.WeChatJSAPI()
|
||||
material = api.WeChatMaterial()
|
||||
oauth = api.WeChatOAuth()
|
||||
shakearound = api.WeChatShakeAround()
|
||||
service = api.WeChatService()
|
||||
chat = api.WeChatChat()
|
||||
|
||||
def __init__(self, corp_id, secret, access_token=None,
|
||||
session=None, timeout=None, auto_retry=True):
|
||||
super(WeChatClient, self).__init__(
|
||||
corp_id, access_token, session, timeout, auto_retry
|
||||
)
|
||||
self.corp_id = corp_id
|
||||
self.secret = secret
|
||||
|
||||
def fetch_access_token(self):
|
||||
""" Fetch access token"""
|
||||
return self._fetch_access_token(
|
||||
url='https://qyapi.weixin.qq.com/cgi-bin/gettoken',
|
||||
params={
|
||||
'corpid': self.corp_id,
|
||||
'corpsecret': self.secret
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.enterprise.client.api.department import WeChatDepartment # NOQA
|
||||
from wechatpy.enterprise.client.api.media import WeChatMedia # NOQA
|
||||
from wechatpy.enterprise.client.api.message import WeChatMessage # NOQA
|
||||
from wechatpy.enterprise.client.api.menu import WeChatMenu # NOQA
|
||||
from wechatpy.enterprise.client.api.tag import WeChatTag # NOQA
|
||||
from wechatpy.enterprise.client.api.user import WeChatUser # NOQA
|
||||
from wechatpy.enterprise.client.api.misc import WeChatMisc # NOQA
|
||||
from wechatpy.enterprise.client.api.agent import WeChatAgent # NOQA
|
||||
from wechatpy.enterprise.client.api.batch import WeChatBatch # NOQA
|
||||
from wechatpy.enterprise.client.api.jsapi import WeChatJSAPI # NOQA
|
||||
from wechatpy.enterprise.client.api.material import WeChatMaterial # NOQA
|
||||
from wechatpy.enterprise.client.api.oauth import WeChatOAuth # NOQA
|
||||
from wechatpy.enterprise.client.api.shakearound import WeChatShakeAround # NOQA
|
||||
from wechatpy.enterprise.client.api.service import WeChatService # NOQA
|
||||
from wechatpy.enterprise.client.api.chat import WeChatChat # NOQA
|
||||
71
sg_wechat_enterprise/we_api/enterprise/client/api/agent.py
Normal file
71
sg_wechat_enterprise/we_api/enterprise/client/api/agent.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatAgent(BaseWeChatAPI):
|
||||
|
||||
def get(self, agent_id):
|
||||
"""
|
||||
获取企业号应用
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=获取企业号应用
|
||||
|
||||
:param agent_id: 授权方应用 id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'agent/get',
|
||||
params={
|
||||
'agentid': agent_id
|
||||
}
|
||||
)
|
||||
|
||||
def set(self,
|
||||
agent_id,
|
||||
name=None,
|
||||
description=None,
|
||||
redirect_domain=None,
|
||||
logo_media_id=None,
|
||||
report_location_flag=0,
|
||||
is_report_user=True,
|
||||
is_report_enter=True):
|
||||
"""
|
||||
设置企业号应用
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=设置企业号应用
|
||||
|
||||
:param agent_id: 企业应用的 id
|
||||
:param name: 企业应用名称
|
||||
:param description: 企业应用详情
|
||||
:param redirect_domain: 企业应用可信域名
|
||||
:param logo_media_id: 企业应用头像的mediaid,通过多媒体接口上传图片获得mediaid
|
||||
:param report_location_flag: 企业应用是否打开地理位置上报 0:不上报;1:进入会话上报;2:持续上报
|
||||
:param is_report_user: 是否接收用户变更通知
|
||||
:param is_report_enter: 是否上报用户进入应用事件
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
agent_data = optionaldict()
|
||||
agent_data['agentid'] = agent_id
|
||||
agent_data['name'] = name
|
||||
agent_data['description'] = description
|
||||
agent_data['redirect_domain'] = redirect_domain
|
||||
agent_data['logo_mediaid'] = logo_media_id
|
||||
agent_data['report_location_flag'] = report_location_flag
|
||||
agent_data['isreportuser'] = 1 if is_report_user else 0
|
||||
agent_data['isreportenter'] = 1 if is_report_enter else 0
|
||||
return self._post(
|
||||
'agent/set',
|
||||
data=agent_data
|
||||
)
|
||||
|
||||
def list(self):
|
||||
"""
|
||||
获取应用概况列表
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=获取应用概况列表
|
||||
|
||||
:return: 应用概况列表
|
||||
"""
|
||||
res = self._get('agent/list')
|
||||
return res['agentlist']
|
||||
134
sg_wechat_enterprise/we_api/enterprise/client/api/batch.py
Normal file
134
sg_wechat_enterprise/we_api/enterprise/client/api/batch.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
from wechatpy.utils import to_text
|
||||
|
||||
|
||||
class WeChatBatch(BaseWeChatAPI):
|
||||
|
||||
def invite_user(self, url, token, encoding_aes_key, user_ids=None,
|
||||
party_ids=None, tag_ids=None, invite_tips=None):
|
||||
"""
|
||||
邀请成员关注
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=异步任务接口
|
||||
|
||||
:param url: 企业应用接收企业号推送请求的访问协议和地址,支持http或https协议
|
||||
:param token: 用于生成签名
|
||||
:param encoding_aes_key: 用于消息体的加密,是AES密钥的Base64编码
|
||||
:param user_ids: 可选,成员ID列表,多个接收者用‘|’分隔,最多支持1000个。
|
||||
:param party_ids: 可选,部门ID列表,多个接收者用‘|’分隔,最多支持100个。
|
||||
:param tag_ids: 可选,标签ID列表,多个接收者用‘|’分隔。
|
||||
:param invite_tips: 可选,推送到微信上的提示语
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict()
|
||||
data['callback'] = {
|
||||
'url': url,
|
||||
'token': token,
|
||||
'encodingaeskey': encoding_aes_key
|
||||
}
|
||||
if isinstance(user_ids, (tuple, list)):
|
||||
user_ids = '|'.join(map(to_text, user_ids))
|
||||
if isinstance(party_ids, (tuple, list)):
|
||||
party_ids = '|'.join(map(to_text, party_ids))
|
||||
if isinstance(tag_ids, (tuple, list)):
|
||||
tag_ids = '|'.join(map(to_text, tag_ids))
|
||||
data['touser'] = user_ids
|
||||
data['toparty'] = party_ids
|
||||
data['totag'] = tag_ids
|
||||
data['invite_tips'] = invite_tips
|
||||
return self._post(
|
||||
'batch/inviteuser',
|
||||
data=data
|
||||
)
|
||||
|
||||
def sync_user(self, url, token, encoding_aes_key, media_id):
|
||||
"""
|
||||
增量更新成员
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=异步任务接口
|
||||
|
||||
:param url: 企业应用接收企业号推送请求的访问协议和地址,支持http或https协议
|
||||
:param token: 用于生成签名
|
||||
:param encoding_aes_key: 用于消息体的加密,是AES密钥的Base64编码
|
||||
:param media_id: 上传的csv文件的media_id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'batch/syncuser',
|
||||
data={
|
||||
'media_id': media_id,
|
||||
'callback': {
|
||||
'url': url,
|
||||
'token': token,
|
||||
'encodingaeskey': encoding_aes_key
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def replace_user(self, url, token, encoding_aes_key, media_id):
|
||||
"""
|
||||
全量覆盖成员
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=异步任务接口
|
||||
|
||||
:param url: 企业应用接收企业号推送请求的访问协议和地址,支持http或https协议
|
||||
:param token: 用于生成签名
|
||||
:param encoding_aes_key: 用于消息体的加密,是AES密钥的Base64编码
|
||||
:param media_id: 上传的csv文件的media_id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'batch/replaceuser',
|
||||
data={
|
||||
'media_id': media_id,
|
||||
'callback': {
|
||||
'url': url,
|
||||
'token': token,
|
||||
'encodingaeskey': encoding_aes_key
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def replace_party(self, url, token, encoding_aes_key, media_id):
|
||||
"""
|
||||
全量覆盖部门
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=异步任务接口
|
||||
|
||||
:param url: 企业应用接收企业号推送请求的访问协议和地址,支持http或https协议
|
||||
:param token: 用于生成签名
|
||||
:param encoding_aes_key: 用于消息体的加密,是AES密钥的Base64编码
|
||||
:param media_id: 上传的csv文件的media_id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'batch/replaceparty',
|
||||
data={
|
||||
'media_id': media_id,
|
||||
'callback': {
|
||||
'url': url,
|
||||
'token': token,
|
||||
'encodingaeskey': encoding_aes_key
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def get_result(self, job_id):
|
||||
"""
|
||||
获取异步任务结果
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=异步任务接口
|
||||
|
||||
:param job_id: 异步任务id,最大长度为64字符
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'batch/getresult',
|
||||
params={
|
||||
'jobid': job_id
|
||||
}
|
||||
)
|
||||
273
sg_wechat_enterprise/we_api/enterprise/client/api/chat.py
Normal file
273
sg_wechat_enterprise/we_api/enterprise/client/api/chat.py
Normal file
@@ -0,0 +1,273 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatChat(BaseWeChatAPI):
|
||||
|
||||
def create(self, chat_id, name, owner, user_list):
|
||||
"""
|
||||
创建会话
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param chat_id: 会话id。字符串类型,最长32个字符。只允许字符0-9及字母a-zA-Z,
|
||||
如果值内容为64bit无符号整型:要求值范围在[1, 2^63)之间,
|
||||
[2^63, 2^64)为系统分配会话id区间
|
||||
:param name: 会话标题
|
||||
:param owner: 管理员userid,必须是该会话userlist的成员之一
|
||||
:param user_list: 会话成员列表,成员用userid来标识。会话成员必须在3人或以上,1000人以下
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'chat/create',
|
||||
data={
|
||||
'chatid': chat_id,
|
||||
'name': name,
|
||||
'owner': owner,
|
||||
'userlist': user_list,
|
||||
}
|
||||
)
|
||||
|
||||
def get(self, chat_id):
|
||||
"""
|
||||
获取会话
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param chat_id: 会话 ID
|
||||
:return: 会话信息
|
||||
"""
|
||||
res = self._get('chat/get', params={'chatid': chat_id})
|
||||
return res['chat_info']
|
||||
|
||||
def update(self, chat_id, op_user, name=None, owner=None,
|
||||
add_user_list=None, del_user_list=None):
|
||||
"""
|
||||
修改会话
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param chat_id: 会话 ID
|
||||
:param op_user: 操作人 userid
|
||||
:param name: 会话标题
|
||||
:param owner: 管理员userid,必须是该会话userlist的成员之一
|
||||
:param add_user_list: 会话新增成员列表,成员用userid来标识
|
||||
:param del_user_list: 会话退出成员列表,成员用userid来标识
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict(
|
||||
chatid=chat_id,
|
||||
op_user=op_user,
|
||||
name=name,
|
||||
owner=owner,
|
||||
add_user_list=add_user_list,
|
||||
del_user_list=del_user_list,
|
||||
)
|
||||
return self._post('chat/update', data=data)
|
||||
|
||||
def quit(self, chat_id, op_user):
|
||||
"""
|
||||
退出会话
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param chat_id: 会话 ID
|
||||
:param op_user: 操作人 userid
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'chat/quit',
|
||||
data={
|
||||
'chatid': chat_id,
|
||||
'op_user': op_user,
|
||||
}
|
||||
)
|
||||
|
||||
def clear_notify(self, op_user, type, id):
|
||||
"""
|
||||
清除会话未读状态
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param op_user: 会话所有者的userid
|
||||
:param type: 会话类型:single|group,分别表示:单聊|群聊
|
||||
:param id: 会话值,为userid|chatid,分别表示:成员id|会话id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'chat/clearnotify',
|
||||
data={
|
||||
'op_user': op_user,
|
||||
'chat': {
|
||||
'type': type,
|
||||
'id': id,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def set_mute(self, user_mute_list):
|
||||
"""
|
||||
设置成员新消息免打扰
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param user_mute_list: 成员新消息免打扰参数,数组,最大支持10000个成员
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'chat/setmute',
|
||||
data={'user_mute_list': user_mute_list}
|
||||
)
|
||||
|
||||
def send_text(self, sender, receiver_type, receiver_id, content):
|
||||
"""
|
||||
发送文本消息
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver_type: 接收人类型:single|group,分别表示:单聊|群聊
|
||||
:param receiver_id: 接收人的值,为userid|chatid,分别表示:成员id|会话id
|
||||
:param content: 消息内容
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = {
|
||||
'receiver': {
|
||||
'type': receiver_type,
|
||||
'id': receiver_id,
|
||||
},
|
||||
'sender': sender,
|
||||
'msgtype': 'text',
|
||||
'text': {
|
||||
'content': content,
|
||||
}
|
||||
}
|
||||
return self._post('chat/send', data=data)
|
||||
|
||||
def send_single_text(self, sender, receiver, content):
|
||||
"""
|
||||
发送单聊文本消息
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver: 接收人成员 ID
|
||||
:param content: 消息内容
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self.send_text(sender, 'single', receiver, content)
|
||||
|
||||
def send_group_text(self, sender, receiver, content):
|
||||
"""
|
||||
发送群聊文本消息
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver: 会话 ID
|
||||
:param content: 消息内容
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self.send_text(sender, 'group', receiver, content)
|
||||
|
||||
def send_image(self, sender, receiver_type, receiver_id, media_id):
|
||||
"""
|
||||
发送图片消息
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver_type: 接收人类型:single|group,分别表示:单聊|群聊
|
||||
:param receiver_id: 接收人的值,为userid|chatid,分别表示:成员id|会话id
|
||||
:param media_id: 图片媒体文件id,可以调用上传素材文件接口获取
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = {
|
||||
'receiver': {
|
||||
'type': receiver_type,
|
||||
'id': receiver_id,
|
||||
},
|
||||
'sender': sender,
|
||||
'msgtype': 'image',
|
||||
'image': {
|
||||
'media_id': media_id,
|
||||
}
|
||||
}
|
||||
return self._post('chat/send', data=data)
|
||||
|
||||
def send_single_image(self, sender, receiver, media_id):
|
||||
"""
|
||||
发送单聊图片消息
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver: 接收人成员 ID
|
||||
:param media_id: 图片媒体文件id,可以调用上传素材文件接口获取
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self.send_image(sender, 'single', receiver, media_id)
|
||||
|
||||
def send_group_image(self, sender, receiver, media_id):
|
||||
"""
|
||||
发送群聊图片消息
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver: 会话 ID
|
||||
:param media_id: 图片媒体文件id,可以调用上传素材文件接口获取
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self.send_image(sender, 'group', receiver, media_id)
|
||||
|
||||
def send_file(self, sender, receiver_type, receiver_id, media_id):
|
||||
"""
|
||||
发送文件消息
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=企业会话接口说明
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver_type: 接收人类型:single|group,分别表示:单聊|群聊
|
||||
:param receiver_id: 接收人的值,为userid|chatid,分别表示:成员id|会话id
|
||||
:param media_id: 文件id,可以调用上传素材文件接口获取, 文件须大于4字节
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = {
|
||||
'receiver': {
|
||||
'type': receiver_type,
|
||||
'id': receiver_id,
|
||||
},
|
||||
'sender': sender,
|
||||
'msgtype': 'file',
|
||||
'file': {
|
||||
'media_id': media_id,
|
||||
}
|
||||
}
|
||||
return self._post('chat/send', data=data)
|
||||
|
||||
def send_single_file(self, sender, receiver, media_id):
|
||||
"""
|
||||
发送单聊文件消息
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver: 接收人成员 ID
|
||||
:param media_id: 文件id,可以调用上传素材文件接口获取, 文件须大于4字节
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self.send_file(sender, 'single', receiver, media_id)
|
||||
|
||||
def send_group_file(self, sender, receiver, media_id):
|
||||
"""
|
||||
发送群聊文件消息
|
||||
|
||||
:param sender: 发送人
|
||||
:param receiver: 会话 ID
|
||||
:param media_id: 文件id,可以调用上传素材文件接口获取, 文件须大于4字节
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self.send_file(sender, 'group', receiver, media_id)
|
||||
117
sg_wechat_enterprise/we_api/enterprise/client/api/department.py
Normal file
117
sg_wechat_enterprise/we_api/enterprise/client/api/department.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatDepartment(BaseWeChatAPI):
|
||||
|
||||
def create(self, name, parent_id=1, order=None, id=None):
|
||||
"""
|
||||
创建部门
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理部门
|
||||
|
||||
:param name: 部门名称,长度限制为 1~64 个字符
|
||||
:param parent_id: 父亲部门 id ,根部门 id 为 1
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
department_data = optionaldict()
|
||||
department_data['name'] = name
|
||||
department_data['parentid'] = parent_id
|
||||
department_data['order'] = order
|
||||
department_data['id'] = id
|
||||
return self._post(
|
||||
'department/create',
|
||||
data=dict(department_data)
|
||||
)
|
||||
|
||||
def update(self, id, name=None, parent_id=None, order=None):
|
||||
"""
|
||||
更新部门
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理部门
|
||||
|
||||
:param id: 部门 id
|
||||
:param name: 部门名称
|
||||
:param parent_id: 父亲部门 id
|
||||
:param order: 在父部门中的次序,从 1 开始,数字越大排序越靠后
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
department_data = optionaldict()
|
||||
department_data['id'] = id
|
||||
department_data['name'] = name
|
||||
department_data['parentid'] = parent_id
|
||||
department_data['order'] = order
|
||||
return self._post(
|
||||
'department/update',
|
||||
data=dict(department_data)
|
||||
)
|
||||
|
||||
def delete(self, id):
|
||||
"""
|
||||
删除部门
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理部门
|
||||
|
||||
:param id: 部门 id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'department/delete',
|
||||
params={
|
||||
'id': id
|
||||
}
|
||||
)
|
||||
|
||||
def get(self):
|
||||
"""
|
||||
获取部门列表
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理部门
|
||||
|
||||
:return: 部门列表
|
||||
"""
|
||||
res = self._get('department/list')
|
||||
return res['department']
|
||||
|
||||
def get_users(self, id, status=0, fetch_child=0):
|
||||
"""
|
||||
获取部门成员列表
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理成员
|
||||
|
||||
:param id: 部门 id
|
||||
:param status: 0 获取全部员工,1 获取已关注成员列表,
|
||||
2 获取禁用成员列表,4 获取未关注成员列表。可叠加
|
||||
:param fetch_child: 1/0:是否递归获取子部门下面的成员
|
||||
:return: 部门成员列表
|
||||
"""
|
||||
fetch_child = 1 if fetch_child else 0
|
||||
res = self._get(
|
||||
'user/simplelist',
|
||||
params={
|
||||
'department_id': id,
|
||||
'status': status,
|
||||
'fetch_child': fetch_child
|
||||
}
|
||||
)
|
||||
return res['userlist']
|
||||
|
||||
def get_users_list(self, id, status=0, fetch_child=0):
|
||||
"""
|
||||
获取部门成员详情列表
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理成员
|
||||
|
||||
:param id: 部门 id
|
||||
:param status: 0 获取全部员工,1 获取已关注成员列表,
|
||||
2 获取禁用成员列表,4 获取未关注成员列表。可叠加
|
||||
:param fetch_child: 1/0:是否递归获取子部门下面的成员
|
||||
:return: 部门成员列表
|
||||
"""
|
||||
fetch_child = 1 if fetch_child else 0
|
||||
res = self._get(
|
||||
'user/list',
|
||||
params={
|
||||
'department_id': id,
|
||||
'status': status,
|
||||
'fetch_child': fetch_child
|
||||
}
|
||||
)
|
||||
return res['userlist']
|
||||
48
sg_wechat_enterprise/we_api/enterprise/client/api/jsapi.py
Normal file
48
sg_wechat_enterprise/we_api/enterprise/client/api/jsapi.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
from wechatpy.utils import WeChatSigner
|
||||
|
||||
|
||||
class WeChatJSAPI(BaseWeChatAPI):
|
||||
|
||||
def get_ticket(self):
|
||||
"""
|
||||
获取微信 JS-SDK ticket
|
||||
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E5%BE%AE%E4%BF%A1JS%E6%8E%A5%E5%8F%A3
|
||||
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get('get_jsapi_ticket')
|
||||
|
||||
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()
|
||||
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
|
||||
201
sg_wechat_enterprise/we_api/enterprise/client/api/material.py
Normal file
201
sg_wechat_enterprise/we_api/enterprise/client/api/material.py
Normal file
@@ -0,0 +1,201 @@
|
||||
# encoding: utf-8
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import requests
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMaterial(BaseWeChatAPI):
|
||||
|
||||
def add_articles(self, articles):
|
||||
"""
|
||||
新增永久图文素材
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E4%B8%8A%E4%BC%A0%E6%B0%B8%E4%B9%85%E7%B4%A0%E6%9D%90
|
||||
|
||||
: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_mpnews',
|
||||
data={
|
||||
"mpnews": {
|
||||
"articles": articles_data
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def add(self, agent_id, media_type, media_file):
|
||||
"""
|
||||
新增其它类型永久素材
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E4%B8%8A%E4%BC%A0%E6%B0%B8%E4%B9%85%E7%B4%A0%E6%9D%90
|
||||
|
||||
:param agent_id: 企业应用的id
|
||||
:param media_type: 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)普通文件(file)
|
||||
:param media_file: 要上传的文件,一个 File-object
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
params = {
|
||||
'agentid': agent_id,
|
||||
'type': media_type,
|
||||
}
|
||||
return self._post(
|
||||
url='material/add_material',
|
||||
params=params,
|
||||
files={
|
||||
'media': media_file
|
||||
}
|
||||
)
|
||||
|
||||
def get_url(self, agent_id, media_id):
|
||||
"""
|
||||
获取永久素材下载地址
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E6%B0%B8%E4%B9%85%E7%B4%A0%E6%9D%90
|
||||
|
||||
:param agent_id: 企业应用的id
|
||||
:param media_id: 媒体文件 ID
|
||||
:return: 临时素材下载地址
|
||||
"""
|
||||
parts = (
|
||||
'https://qyapi.weixin.qq.com/cgi-bin/material/get',
|
||||
'?access_token=',
|
||||
self.access_token,
|
||||
'&media_id=',
|
||||
media_id,
|
||||
'&agentid=',
|
||||
agent_id,
|
||||
)
|
||||
return ''.join(parts)
|
||||
|
||||
def get(self, agent_id, media_id):
|
||||
"""
|
||||
获取永久素材
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E6%B0%B8%E4%B9%85%E7%B4%A0%E6%9D%90
|
||||
|
||||
:param agent_id: 企业应用的id
|
||||
:param media_id: 媒体文件 ID
|
||||
:return: requests 的 Response 实例
|
||||
"""
|
||||
res = requests.get(self.get_url(agent_id, media_id))
|
||||
|
||||
return res
|
||||
|
||||
def get_articles(self, agent_id, media_id):
|
||||
"""
|
||||
获取永久素材:图文消息素材
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E6%B0%B8%E4%B9%85%E7%B4%A0%E6%9D%90
|
||||
|
||||
:param agent_id: 企业应用的id
|
||||
:param media_id: 媒体文件 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'material/get',
|
||||
params={
|
||||
'agentid': agent_id,
|
||||
'media_id': media_id,
|
||||
}
|
||||
)
|
||||
|
||||
def delete(self, agent_id, media_id):
|
||||
"""
|
||||
删除永久素材
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E5%88%A0%E9%99%A4%E6%B0%B8%E4%B9%85%E7%B4%A0%E6%9D%90
|
||||
|
||||
:param agent_id: 企业应用的id
|
||||
:param media_id: 媒体文件 ID
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'material/del',
|
||||
params={
|
||||
'agentid': agent_id,
|
||||
'media_id': media_id,
|
||||
}
|
||||
)
|
||||
|
||||
def update_articles(self, agent_id, media_id, articles):
|
||||
"""
|
||||
修改永久图文素材
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E4%BF%AE%E6%94%B9%E6%B0%B8%E4%B9%85%E5%9B%BE%E6%96%87%E7%B4%A0%E6%9D%90
|
||||
|
||||
: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={
|
||||
'agentid': agent_id,
|
||||
'media_id': media_id,
|
||||
'articles': articles_data
|
||||
}
|
||||
)
|
||||
|
||||
def get_count(self, agent_id):
|
||||
"""
|
||||
获取素材总数
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E7%B4%A0%E6%9D%90%E6%80%BB%E6%95%B0
|
||||
|
||||
:param agent_id: 企业应用的id
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._get(
|
||||
'material/get_count',
|
||||
params={
|
||||
'agent_id': agent_id,
|
||||
}
|
||||
)
|
||||
|
||||
def batchget(self, agent_id, media_type, offset=0, count=20):
|
||||
"""
|
||||
批量获取永久素材列表
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E7%B4%A0%E6%9D%90%E5%88%97%E8%A1%A8
|
||||
|
||||
:param agent_id: 企业应用的id
|
||||
:param media_type: 媒体文件类型,分别有图文(mpnews)、图片(image)、
|
||||
语音(voice)、视频(video)和文件(file)
|
||||
:param offset: 从全部素材的该偏移位置开始返回,0 表示从第一个素材返回
|
||||
:param count: 返回素材的数量,取值在1到20之间
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'material/batchget',
|
||||
data={
|
||||
'agent_id': agent_id,
|
||||
'type': media_type,
|
||||
'offset': offset,
|
||||
'count': count
|
||||
}
|
||||
)
|
||||
73
sg_wechat_enterprise/we_api/enterprise/client/api/media.py
Normal file
73
sg_wechat_enterprise/we_api/enterprise/client/api/media.py
Normal file
@@ -0,0 +1,73 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import requests
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMedia(BaseWeChatAPI):
|
||||
|
||||
def upload(self, media_type, media_file):
|
||||
"""
|
||||
上传临时素材文件
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E4%B8%8A%E4%BC%A0%E4%B8%B4%E6%97%B6%E7%B4%A0%E6%9D%90%E6%96%87%E4%BB%B6
|
||||
|
||||
:param media_type: 媒体文件类型,分别有图片(image)、语音(voice)、视频(video)和普通文件(file)
|
||||
:param media_file: 要上传的文件,一个 File-object
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'media/upload',
|
||||
params={
|
||||
'type': media_type
|
||||
},
|
||||
files={
|
||||
'media': media_file
|
||||
}
|
||||
)
|
||||
|
||||
def upload_image(self, media_file):
|
||||
"""
|
||||
上传卡券logo
|
||||
:param media_file: 要上传的文件
|
||||
:return:
|
||||
"""
|
||||
return self._post(
|
||||
'media/uploadimg',
|
||||
params={
|
||||
'type': 'card_logo'
|
||||
},
|
||||
files={
|
||||
'media': media_file
|
||||
}
|
||||
)
|
||||
|
||||
def download(self, media_id):
|
||||
"""
|
||||
获取临时素材文件
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E4%B8%B4%E6%97%B6%E7%B4%A0%E6%9D%90%E6%96%87%E4%BB%B6
|
||||
|
||||
:param media_id: 媒体文件 ID
|
||||
:return: requests 的 Response 实例
|
||||
"""
|
||||
return requests.get(self.get_url(media_id))
|
||||
|
||||
def get_url(self, media_id):
|
||||
"""
|
||||
获取临时素材下载地址
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=%E8%8E%B7%E5%8F%96%E4%B8%B4%E6%97%B6%E7%B4%A0%E6%9D%90%E6%96%87%E4%BB%B6
|
||||
|
||||
:param media_id: 媒体文件 ID
|
||||
:return: 临时素材下载地址
|
||||
"""
|
||||
parts = (
|
||||
'https://qyapi.weixin.qq.com/cgi-bin/media/get',
|
||||
'?access_token=',
|
||||
self.access_token,
|
||||
'&media_id=',
|
||||
media_id
|
||||
)
|
||||
return ''.join(parts)
|
||||
43
sg_wechat_enterprise/we_api/enterprise/client/api/menu.py
Normal file
43
sg_wechat_enterprise/we_api/enterprise/client/api/menu.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
from wechatpy.exceptions import WeChatClientException
|
||||
|
||||
|
||||
class WeChatMenu(BaseWeChatAPI):
|
||||
|
||||
def create(self, agent_id, menu_data):
|
||||
return self._post(
|
||||
'menu/create',
|
||||
params={
|
||||
'agentid': agent_id
|
||||
},
|
||||
data=menu_data
|
||||
)
|
||||
|
||||
def get(self, agent_id):
|
||||
try:
|
||||
return self._get(
|
||||
'menu/get',
|
||||
params={
|
||||
'agentid': agent_id
|
||||
}
|
||||
)
|
||||
except WeChatClientException as e:
|
||||
if e.errcode == 46003:
|
||||
# menu not exist
|
||||
return None
|
||||
else:
|
||||
raise e
|
||||
|
||||
def delete(self, agent_id):
|
||||
return self._get(
|
||||
'menu/delete',
|
||||
params={
|
||||
'agentid': agent_id
|
||||
}
|
||||
)
|
||||
|
||||
def update(self, agent_id, menu_data):
|
||||
self.delete(agent_id)
|
||||
return self.create(agent_id, menu_data)
|
||||
161
sg_wechat_enterprise/we_api/enterprise/client/api/message.py
Normal file
161
sg_wechat_enterprise/we_api/enterprise/client/api/message.py
Normal file
@@ -0,0 +1,161 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMessage(BaseWeChatAPI):
|
||||
|
||||
def _send_message(self, agent_id, user_ids, party_ids='',
|
||||
tag_ids='', msg=None):
|
||||
msg = msg or {}
|
||||
if isinstance(user_ids, (tuple, list)):
|
||||
user_ids = '|'.join(user_ids)
|
||||
if isinstance(party_ids, (tuple, list)):
|
||||
party_ids = '|'.join(party_ids)
|
||||
if isinstance(tag_ids, (tuple, list)):
|
||||
tag_ids = '|'.join(tag_ids)
|
||||
|
||||
data = {
|
||||
'touser': user_ids,
|
||||
'toparty': party_ids,
|
||||
'totag': tag_ids,
|
||||
'agentid': agent_id
|
||||
}
|
||||
data.update(msg)
|
||||
return self._post(
|
||||
'message/send',
|
||||
data=data
|
||||
)
|
||||
|
||||
def send_text(self, agent_id, user_ids, content,
|
||||
party_ids='', tag_ids='', safe=0):
|
||||
return self._send_message(
|
||||
agent_id,
|
||||
user_ids,
|
||||
party_ids,
|
||||
tag_ids,
|
||||
msg={
|
||||
'msgtype': 'text',
|
||||
'text': {'content': content},
|
||||
'safe': safe
|
||||
}
|
||||
)
|
||||
|
||||
def send_image(self, agent_id, user_ids, media_id,
|
||||
party_ids='', tag_ids='', safe=0):
|
||||
return self._send_message(
|
||||
agent_id,
|
||||
user_ids,
|
||||
party_ids,
|
||||
tag_ids,
|
||||
msg={
|
||||
'msgtype': 'image',
|
||||
'image': {
|
||||
'media_id': media_id
|
||||
},
|
||||
'safe': safe
|
||||
}
|
||||
)
|
||||
|
||||
def send_voice(self, agent_id, user_ids, media_id,
|
||||
party_ids='', tag_ids='', safe=0):
|
||||
return self._send_message(
|
||||
agent_id,
|
||||
user_ids,
|
||||
party_ids,
|
||||
tag_ids,
|
||||
msg={
|
||||
'msgtype': 'voice',
|
||||
'voice': {
|
||||
'media_id': media_id
|
||||
},
|
||||
'safe': safe
|
||||
}
|
||||
)
|
||||
|
||||
def send_video(self, agent_id, user_ids, media_id, title=None,
|
||||
description=None, party_ids='', tag_ids='', safe=0):
|
||||
video_data = optionaldict()
|
||||
video_data['media_id'] = media_id
|
||||
video_data['title'] = title
|
||||
video_data['description'] = description
|
||||
|
||||
return self._send_message(
|
||||
agent_id,
|
||||
user_ids,
|
||||
party_ids,
|
||||
tag_ids,
|
||||
msg={
|
||||
'msgtype': 'video',
|
||||
'video': dict(video_data),
|
||||
'safe': safe
|
||||
}
|
||||
)
|
||||
|
||||
def send_file(self, agent_id, user_ids, media_id,
|
||||
party_ids='', tag_ids='', safe=0):
|
||||
return self._send_message(
|
||||
agent_id,
|
||||
user_ids,
|
||||
party_ids,
|
||||
tag_ids,
|
||||
msg={
|
||||
'msgtype': 'file',
|
||||
'file': {
|
||||
'media_id': media_id
|
||||
},
|
||||
'safe': safe
|
||||
}
|
||||
)
|
||||
|
||||
def send_articles(self, agent_id, user_ids, articles,
|
||||
party_ids='', tag_ids=''):
|
||||
articles_data = []
|
||||
for article in articles:
|
||||
articles_data.append({
|
||||
'title': article['title'],
|
||||
'description': article['description'],
|
||||
'url': article['url'],
|
||||
'picurl': article['image']
|
||||
})
|
||||
return self._send_message(
|
||||
agent_id,
|
||||
user_ids,
|
||||
party_ids,
|
||||
tag_ids,
|
||||
msg={
|
||||
'msgtype': 'news',
|
||||
'news': {
|
||||
'articles': articles_data
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def send_mp_articles(self, agent_id, user_ids, articles,
|
||||
party_ids='', tag_ids='', safe=0):
|
||||
articles_data = []
|
||||
for article in articles:
|
||||
articles_data.append({
|
||||
'thumb_media_id': article['thumb_media_id'],
|
||||
'author': article['author'],
|
||||
'title': article['title'],
|
||||
'content': article['content'],
|
||||
'content_source_url': article['content_source_url'],
|
||||
'digest': article['digest'],
|
||||
'show_cover_pic': article['show_cover_pic']
|
||||
})
|
||||
return self._send_message(
|
||||
agent_id,
|
||||
user_ids,
|
||||
party_ids,
|
||||
tag_ids,
|
||||
msg={
|
||||
'msgtype': 'mpnews',
|
||||
'mpnews': {
|
||||
'articles': articles_data
|
||||
},
|
||||
'safe': safe
|
||||
}
|
||||
)
|
||||
15
sg_wechat_enterprise/we_api/enterprise/client/api/misc.py
Normal file
15
sg_wechat_enterprise/we_api/enterprise/client/api/misc.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatMisc(BaseWeChatAPI):
|
||||
|
||||
def get_wechat_ips(self):
|
||||
"""
|
||||
获取微信服务器 IP 列表
|
||||
|
||||
:return: IP 地址列表
|
||||
"""
|
||||
res = self._get('getcallbackip')
|
||||
return res['ip_list']
|
||||
51
sg_wechat_enterprise/we_api/enterprise/client/api/oauth.py
Normal file
51
sg_wechat_enterprise/we_api/enterprise/client/api/oauth.py
Normal file
@@ -0,0 +1,51 @@
|
||||
# encoding: utf-8
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import six
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatOAuth(BaseWeChatAPI):
|
||||
|
||||
OAUTH_BASE_URL = 'https://open.weixin.qq.com/connect/oauth2/authorize'
|
||||
|
||||
def authorize_url(self, redirect_uri, state=None):
|
||||
"""
|
||||
获取授权地址
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=OAuth%E9%AA%8C%E8%AF%81%E6%8E%A5%E5%8F%A3
|
||||
|
||||
:param redirect_url: 授权后重定向的回调链接地址
|
||||
:param state: 重定向后会带上 state 参数
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
redirect_uri = six.moves.urllib.parse.quote(redirect_uri)
|
||||
url_list = [
|
||||
self.OAUTH_BASE_URL,
|
||||
'?appid=',
|
||||
self._client.corp_id,
|
||||
'&redirect_uri=',
|
||||
redirect_uri,
|
||||
'&response_type=code&scope=snsapi_base',
|
||||
]
|
||||
if state:
|
||||
url_list.extend(['&state=', state])
|
||||
url_list.append('#wechat_redirect')
|
||||
return ''.join(url_list)
|
||||
|
||||
def get_user_info(self, code):
|
||||
"""
|
||||
根据 code 获取用户信息
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=OAuth%E9%AA%8C%E8%AF%81%E6%8E%A5%E5%8F%A3
|
||||
|
||||
:param code: 通过成员授权获取到的code
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
|
||||
return self._get(
|
||||
'user/getuserinfo',
|
||||
params={
|
||||
'code': code,
|
||||
}
|
||||
)
|
||||
45
sg_wechat_enterprise/we_api/enterprise/client/api/service.py
Normal file
45
sg_wechat_enterprise/we_api/enterprise/client/api/service.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatService(BaseWeChatAPI):
|
||||
|
||||
def get_provider_token(self, provider_secret):
|
||||
"""
|
||||
获取应用提供商凭证
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=获取应用提供商凭证
|
||||
|
||||
:param provider_secret: 提供商的secret,在提供商管理页面可见
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'service/get_provider_token',
|
||||
data={
|
||||
'corpid': self._client.corp_id,
|
||||
'provider_secret': provider_secret,
|
||||
}
|
||||
)
|
||||
|
||||
def get_login_info(self, provider_access_token, auth_code):
|
||||
"""
|
||||
获取企业号管理员登录信息
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=获取企业号管理员登录信息
|
||||
|
||||
:param provider_access_token: 服务提供商的 accesstoken
|
||||
:param auth_code: OAuth 2.0 授权企业号管理员登录产生的 code
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
return self._post(
|
||||
'service/get_login_info',
|
||||
params={
|
||||
'provider_access_token': provider_access_token,
|
||||
},
|
||||
data={
|
||||
'auth_code': auth_code,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatShakeAround(BaseWeChatAPI):
|
||||
|
||||
def get_shake_info(self, ticket):
|
||||
"""
|
||||
获取摇周边的设备及用户信息
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=获取设备及用户信息
|
||||
|
||||
:param ticket: 摇周边业务的ticket,可在摇到的 URL 中得到,ticket 生效时间为30分钟
|
||||
:return: 设备及用户信息
|
||||
"""
|
||||
res = self._post(
|
||||
'shakearound/getshakeinfo',
|
||||
data={
|
||||
'ticket': ticket
|
||||
}
|
||||
)
|
||||
return res['data']
|
||||
61
sg_wechat_enterprise/we_api/enterprise/client/api/tag.py
Normal file
61
sg_wechat_enterprise/we_api/enterprise/client/api/tag.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatTag(BaseWeChatAPI):
|
||||
|
||||
def create(self, name):
|
||||
return self._post(
|
||||
'tag/create',
|
||||
data={
|
||||
'tagname': name
|
||||
}
|
||||
)
|
||||
|
||||
def update(self, tag_id, name):
|
||||
return self._post(
|
||||
'tag/update',
|
||||
data={
|
||||
'tagid': tag_id,
|
||||
'tagname': name
|
||||
}
|
||||
)
|
||||
|
||||
def delete(self, tag_id):
|
||||
return self._get(
|
||||
'tag/delete',
|
||||
params={
|
||||
'tagid': tag_id
|
||||
}
|
||||
)
|
||||
|
||||
def get_users(self, tag_id):
|
||||
return self._get(
|
||||
'tag/get',
|
||||
params={
|
||||
'tagid': tag_id
|
||||
}
|
||||
)
|
||||
|
||||
def add_users(self, tag_id, user_ids):
|
||||
return self._post(
|
||||
'tag/addtagusers',
|
||||
data={
|
||||
'tagid': tag_id,
|
||||
'userlist': user_ids
|
||||
}
|
||||
)
|
||||
|
||||
def delete_users(self, tag_id, user_ids):
|
||||
return self._post(
|
||||
'tag/deltagusers',
|
||||
data={
|
||||
'tagid': tag_id,
|
||||
'userlist': user_ids
|
||||
}
|
||||
)
|
||||
|
||||
def list(self):
|
||||
res = self._get('tag/list')
|
||||
return res['taglist']
|
||||
160
sg_wechat_enterprise/we_api/enterprise/client/api/user.py
Normal file
160
sg_wechat_enterprise/we_api/enterprise/client/api/user.py
Normal file
@@ -0,0 +1,160 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.client.api.base import BaseWeChatAPI
|
||||
|
||||
|
||||
class WeChatUser(BaseWeChatAPI):
|
||||
|
||||
def create(self, user_id, name, department=None, position=None,
|
||||
mobile=None, gender=0, tel=None, email=None,
|
||||
weixin_id=None, extattr=None, avatar_mediaid=None, to_invite=True):
|
||||
"""
|
||||
创建成员
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理成员
|
||||
"""
|
||||
user_data = optionaldict()
|
||||
user_data['userid'] = user_id
|
||||
user_data['name'] = name
|
||||
user_data['gender'] = gender
|
||||
user_data['department'] = department
|
||||
user_data['position'] = position
|
||||
user_data['mobile'] = mobile
|
||||
user_data['tel'] = tel
|
||||
user_data['email'] = email
|
||||
user_data['weixinid'] = weixin_id
|
||||
user_data['extattr'] = extattr
|
||||
user_data['avatar_mediaid'] = avatar_mediaid
|
||||
user_data['to_invite'] = to_invite
|
||||
|
||||
return self._post(
|
||||
'user/create',
|
||||
data=user_data
|
||||
)
|
||||
|
||||
def update(self, user_id, name=None, department=None, position=None,
|
||||
mobile=None, gender=None, tel=None, email=None,
|
||||
weixin_id=None, enable=None, extattr=None, avatar_mediaid=None):
|
||||
"""
|
||||
更新成员
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理成员
|
||||
"""
|
||||
user_data = optionaldict()
|
||||
user_data['userid'] = user_id
|
||||
user_data['name'] = name
|
||||
user_data['gender'] = gender
|
||||
user_data['department'] = department
|
||||
user_data['position'] = position
|
||||
user_data['mobile'] = mobile
|
||||
user_data['tel'] = tel
|
||||
user_data['email'] = email
|
||||
user_data['weixinid'] = weixin_id
|
||||
user_data['extattr'] = extattr
|
||||
user_data['enable'] = enable
|
||||
user_data['avatar_mediaid'] = avatar_mediaid
|
||||
|
||||
return self._post(
|
||||
'user/update',
|
||||
data=user_data
|
||||
)
|
||||
|
||||
def delete(self, user_id):
|
||||
"""
|
||||
删除成员
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理成员
|
||||
"""
|
||||
return self._get(
|
||||
'user/delete',
|
||||
params={
|
||||
'userid': user_id
|
||||
}
|
||||
)
|
||||
|
||||
def get(self, user_id):
|
||||
"""
|
||||
获取成员
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理成员
|
||||
"""
|
||||
return self._get(
|
||||
'user/get',
|
||||
params={
|
||||
'userid': user_id
|
||||
}
|
||||
)
|
||||
|
||||
def verify(self, user_id):
|
||||
return self._get(
|
||||
'user/authsucc',
|
||||
params={
|
||||
'userid': user_id
|
||||
}
|
||||
)
|
||||
|
||||
def get_info(self, agent_id, code):
|
||||
return self._get(
|
||||
'user/getuserinfo',
|
||||
params={
|
||||
'agentid': agent_id,
|
||||
'code': code
|
||||
}
|
||||
)
|
||||
|
||||
def batch_delete(self, user_ids):
|
||||
"""
|
||||
批量删除成员
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理成员
|
||||
"""
|
||||
return self._post(
|
||||
'user/batchdelete',
|
||||
data={
|
||||
'useridlist': user_ids
|
||||
}
|
||||
)
|
||||
|
||||
def list(self, department_id, fetch_child=False, status=0):
|
||||
"""
|
||||
批量获取部门成员
|
||||
详情请参考 http://qydev.weixin.qq.com/wiki/index.php?title=管理成员
|
||||
"""
|
||||
res = self._get(
|
||||
'user/list',
|
||||
params={
|
||||
'department_id': department_id,
|
||||
'fetch_child': 1 if fetch_child else 0,
|
||||
'status': status
|
||||
}
|
||||
)
|
||||
return res['userlist']
|
||||
|
||||
def convert_to_openid(self, user_id, agent_id=None):
|
||||
"""
|
||||
user_id 转成 openid
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=Userid%E4%B8%8Eopenid%E4%BA%92%E6%8D%A2%E6%8E%A5%E5%8F%A3
|
||||
|
||||
:param user_id: 企业号内的成员 ID
|
||||
:param agent_id: 可选,需要发送红包的应用ID,若只是使用微信支付和企业转账,则无需该参数
|
||||
:return: 返回的 JSON 数据包
|
||||
"""
|
||||
data = optionaldict()
|
||||
data['userid'] = user_id
|
||||
data['agentid'] = agent_id
|
||||
return self._post('user/convert_to_openid', data=data)
|
||||
|
||||
def convert_to_user_id(self, openid):
|
||||
"""
|
||||
openid 转成 user_id
|
||||
|
||||
详情请参考
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=Userid%E4%B8%8Eopenid%E4%BA%92%E6%8D%A2%E6%8E%A5%E5%8F%A3
|
||||
|
||||
:param openid: 在使用微信支付、微信红包和企业转账之后,返回结果的openid
|
||||
:return: 该 openid 在企业号中对应的成员 user_id
|
||||
"""
|
||||
res = self._post(
|
||||
'user/convert_to_userid',
|
||||
data={'openid': openid}
|
||||
)
|
||||
return res['userid']
|
||||
46
sg_wechat_enterprise/we_api/enterprise/crypto.py
Normal file
46
sg_wechat_enterprise/we_api/enterprise/crypto.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.crypto import BasePrpCrypto, BaseWeChatCrypto
|
||||
from wechatpy.enterprise.exceptions import InvalidCorpIdException
|
||||
|
||||
|
||||
class PrpCrypto(BasePrpCrypto):
|
||||
|
||||
def encrypt(self, text, corp_id):
|
||||
return self._encrypt(text, corp_id)
|
||||
|
||||
def decrypt(self, text, corp_id):
|
||||
return self._decrypt(text, corp_id, InvalidCorpIdException)
|
||||
|
||||
|
||||
class WeChatCrypto(BaseWeChatCrypto):
|
||||
|
||||
def __init__(self, token, encoding_aes_key, corp_id):
|
||||
super(WeChatCrypto, self).__init__(token, encoding_aes_key, corp_id)
|
||||
self.corp_id = corp_id
|
||||
|
||||
def check_signature(self, signature, timestamp, nonce, echo_str):
|
||||
return self._check_signature(
|
||||
signature,
|
||||
timestamp,
|
||||
nonce,
|
||||
echo_str,
|
||||
PrpCrypto
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
163
sg_wechat_enterprise/we_api/enterprise/events.py
Normal file
163
sg_wechat_enterprise/we_api/enterprise/events.py
Normal file
@@ -0,0 +1,163 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.fields import IntegerField, BaseField
|
||||
from wechatpy import events
|
||||
|
||||
|
||||
EVENT_TYPES = {}
|
||||
|
||||
|
||||
def register_event(event_type):
|
||||
def register(cls):
|
||||
EVENT_TYPES[event_type] = cls
|
||||
return cls
|
||||
return register
|
||||
|
||||
|
||||
@register_event('subscribe')
|
||||
class SubscribeEvent(events.SubscribeEvent):
|
||||
"""
|
||||
成员关注事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('unsubscribe')
|
||||
class UnsubscribeEvent(events.UnsubscribeEvent):
|
||||
"""
|
||||
成员取消关注事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('click')
|
||||
class ClickEvent(events.ClickEvent):
|
||||
"""
|
||||
点击菜单拉取消息事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('xml')
|
||||
class ViewEvent(events.ViewEvent):
|
||||
"""
|
||||
点击菜单跳转链接事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('location')
|
||||
class LocationEvent(events.LocationEvent):
|
||||
"""
|
||||
上报地理位置事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('scancode_push')
|
||||
class ScanCodePushEvent(events.ScanCodePushEvent):
|
||||
"""
|
||||
扫码推事件的事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('scancode_waitmsg')
|
||||
class ScanCodeWaitMsgEvent(events.ScanCodeWaitMsgEvent):
|
||||
"""
|
||||
扫码推事件且弹出“消息接收中”提示框的事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('pic_sysphoto')
|
||||
class PicSysPhotoEvent(events.PicSysPhotoEvent):
|
||||
"""
|
||||
弹出系统拍照发图事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('pic_photo_or_album')
|
||||
class PicPhotoOrAlbumEvent(events.PicPhotoOrAlbumEvent):
|
||||
"""
|
||||
弹出拍照或相册发图事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('pic_weixin')
|
||||
class PicWeChatEvent(events.PicWeChatEvent):
|
||||
"""
|
||||
弹出微信相册发图器事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('location_select')
|
||||
class LocationSelectEvent(events.LocationSelectEvent):
|
||||
"""
|
||||
弹出地理位置选择器事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_event('enter_agent')
|
||||
class EnterAgentEvent(events.BaseEvent):
|
||||
"""
|
||||
用户进入应用的事件推送
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
agent = IntegerField('AgentID', 0)
|
||||
event = 'enter_agent'
|
||||
|
||||
|
||||
@register_event('batch_job_result')
|
||||
class BatchJobResultEvent(events.BaseEvent):
|
||||
"""
|
||||
异步任务完成事件
|
||||
详情请参阅
|
||||
http://qydev.weixin.qq.com/wiki/index.php?title=接受事件
|
||||
"""
|
||||
event = 'batch_job_result'
|
||||
batch_job = BaseField('BatchJob')
|
||||
|
||||
@property
|
||||
def job_id(self):
|
||||
return self.batch_job['JobId']
|
||||
|
||||
@property
|
||||
def job_type(self):
|
||||
return self.batch_job['JobType']
|
||||
|
||||
@property
|
||||
def err_code(self):
|
||||
return self.batch_job['ErrCode']
|
||||
|
||||
@property
|
||||
def err_msg(self):
|
||||
return self.batch_job['ErrMsg']
|
||||
9
sg_wechat_enterprise/we_api/enterprise/exceptions.py
Normal file
9
sg_wechat_enterprise/we_api/enterprise/exceptions.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.exceptions import WeChatException
|
||||
|
||||
|
||||
class InvalidCorpIdException(WeChatException):
|
||||
|
||||
def __init__(self, errcode=-40005, errmsg='Invalid corp_id'):
|
||||
super(InvalidCorpIdException, self).__init__(errcode, errmsg)
|
||||
50
sg_wechat_enterprise/we_api/enterprise/messages.py
Normal file
50
sg_wechat_enterprise/we_api/enterprise/messages.py
Normal file
@@ -0,0 +1,50 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.fields import IntegerField, StringField
|
||||
from wechatpy import messages
|
||||
|
||||
|
||||
MESSAGE_TYPES = {}
|
||||
|
||||
|
||||
def register_message(msg_type):
|
||||
def register(cls):
|
||||
MESSAGE_TYPES[msg_type] = cls
|
||||
return cls
|
||||
return register
|
||||
|
||||
|
||||
@register_message('text')
|
||||
class TextMessage(messages.TextMessage):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_message('image')
|
||||
class ImageMessage(messages.ImageMessage):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_message('voice')
|
||||
class VoiceMessage(messages.VoiceMessage):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_message('shortvideo')
|
||||
class ShortVideoMessage(messages.ShortVideoMessage):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_message('video')
|
||||
class VideoMessage(messages.VideoMessage):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_message('location')
|
||||
class LocationMessage(messages.LocationMessage):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
@register_message('link')
|
||||
class LinkMessage(messages.LinkMessage):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
pic_url = StringField('PicUrl')
|
||||
21
sg_wechat_enterprise/we_api/enterprise/parser.py
Normal file
21
sg_wechat_enterprise/we_api/enterprise/parser.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import xmltodict
|
||||
|
||||
from wechatpy.messages import UnknownMessage
|
||||
from wechatpy.utils import to_text
|
||||
from wechatpy.enterprise.messages import MESSAGE_TYPES
|
||||
from wechatpy.enterprise.events import EVENT_TYPES
|
||||
|
||||
|
||||
def parse_message(xml):
|
||||
if not xml:
|
||||
return
|
||||
message = xmltodict.parse(to_text(xml))['xml']
|
||||
message_type = message['MsgType'].lower()
|
||||
if message_type == 'event':
|
||||
event_type = message['Event'].lower()
|
||||
message_class = EVENT_TYPES.get(event_type, UnknownMessage)
|
||||
else:
|
||||
message_class = MESSAGE_TYPES.get(message_type, UnknownMessage)
|
||||
return message_class(message)
|
||||
67
sg_wechat_enterprise/we_api/enterprise/replies.py
Normal file
67
sg_wechat_enterprise/we_api/enterprise/replies.py
Normal file
@@ -0,0 +1,67 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import six
|
||||
|
||||
from wechatpy import replies
|
||||
from wechatpy.fields import IntegerField
|
||||
|
||||
|
||||
REPLY_TYPES = {}
|
||||
|
||||
|
||||
def register_reply(reply_type):
|
||||
def register(cls):
|
||||
REPLY_TYPES[reply_type] = cls
|
||||
return cls
|
||||
return register
|
||||
|
||||
|
||||
@register_reply('text')
|
||||
class TextReply(replies.TextReply):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_reply('image')
|
||||
class ImageReply(replies.ImageReply):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_reply('voice')
|
||||
class VoiceReply(replies.VoiceReply):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_reply('video')
|
||||
class VideoReply(replies.VideoReply):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
@register_reply('news')
|
||||
class ArticlesReply(replies.ArticlesReply):
|
||||
agent = IntegerField('AgentID', 0)
|
||||
|
||||
|
||||
def create_reply(reply, message=None, render=False):
|
||||
r = None
|
||||
if isinstance(reply, replies.BaseReply):
|
||||
r = reply
|
||||
if message:
|
||||
r.source = message.target
|
||||
r.target = message.source
|
||||
r.agent = message.agent
|
||||
elif isinstance(reply, six.string_types):
|
||||
r = TextReply(
|
||||
message=message,
|
||||
content=reply
|
||||
)
|
||||
elif isinstance(reply, (tuple, list)):
|
||||
if len(reply) > 10:
|
||||
raise AttributeError("Can't add more than 10 articles"
|
||||
" in an ArticlesReply")
|
||||
r = ArticlesReply(
|
||||
message=message,
|
||||
articles=reply
|
||||
)
|
||||
if r and render:
|
||||
return r.render()
|
||||
return r
|
||||
629
sg_wechat_enterprise/we_api/events.py
Normal file
629
sg_wechat_enterprise/we_api/events.py
Normal file
@@ -0,0 +1,629 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.events
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This module contains all the events WeChat callback uses.
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
from wechatpy.fields import (
|
||||
StringField,
|
||||
FloatField,
|
||||
IntegerField,
|
||||
BaseField,
|
||||
Base64DecodeField,
|
||||
DateTimeField
|
||||
)
|
||||
from wechatpy.messages import BaseMessage
|
||||
|
||||
|
||||
EVENT_TYPES = {}
|
||||
|
||||
|
||||
def register_event(event_type):
|
||||
"""
|
||||
Register the event class so that they can be accessed from EVENT_TYPES
|
||||
|
||||
:param event_type: Event type
|
||||
"""
|
||||
def register(cls):
|
||||
EVENT_TYPES[event_type] = cls
|
||||
return cls
|
||||
return register
|
||||
|
||||
|
||||
class BaseEvent(BaseMessage):
|
||||
"""Base class for all events"""
|
||||
type = 'event'
|
||||
event = ''
|
||||
|
||||
|
||||
@register_event('subscribe')
|
||||
class SubscribeEvent(BaseEvent):
|
||||
"""
|
||||
用户关注事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html
|
||||
"""
|
||||
event = 'subscribe'
|
||||
|
||||
|
||||
@register_event('unsubscribe')
|
||||
class UnsubscribeEvent(BaseEvent):
|
||||
"""
|
||||
用户取消关注事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html
|
||||
"""
|
||||
event = 'unsubscribe'
|
||||
|
||||
|
||||
@register_event('subscribe_scan')
|
||||
class SubscribeScanEvent(BaseEvent):
|
||||
"""
|
||||
用户扫描二维码关注事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html
|
||||
"""
|
||||
event = 'subscribe_scan'
|
||||
scene_id = StringField('EventKey')
|
||||
ticket = StringField('Ticket')
|
||||
|
||||
|
||||
@register_event('scan')
|
||||
class ScanEvent(BaseEvent):
|
||||
"""
|
||||
用户扫描二维码事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html
|
||||
"""
|
||||
event = 'scan'
|
||||
scene_id = StringField('EventKey')
|
||||
ticket = StringField('Ticket')
|
||||
|
||||
|
||||
@register_event('location')
|
||||
class LocationEvent(BaseEvent):
|
||||
"""
|
||||
上报地理位置事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html
|
||||
"""
|
||||
event = 'location'
|
||||
latitude = FloatField('Latitude', 0.0)
|
||||
longitude = FloatField('Longitude', 0.0)
|
||||
precision = FloatField('Precision', 0.0)
|
||||
|
||||
|
||||
@register_event('click')
|
||||
class ClickEvent(BaseEvent):
|
||||
"""
|
||||
点击菜单拉取消息事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html
|
||||
"""
|
||||
event = 'click'
|
||||
key = StringField('EventKey')
|
||||
|
||||
|
||||
@register_event('xml')
|
||||
class ViewEvent(BaseEvent):
|
||||
"""
|
||||
点击菜单跳转链接事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/2/5baf56ce4947d35003b86a9805634b1e.html
|
||||
"""
|
||||
event = 'xml'
|
||||
url = StringField('EventKey')
|
||||
|
||||
|
||||
@register_event('masssendjobfinish')
|
||||
class MassSendJobFinishEvent(BaseEvent):
|
||||
"""
|
||||
群发消息任务完成事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/15/5380a4e6f02f2ffdc7981a8ed7a40753.html
|
||||
"""
|
||||
id = IntegerField('MsgID', 0)
|
||||
event = 'masssendjobfinish'
|
||||
status = StringField('Status')
|
||||
total_count = IntegerField('TotalCount', 0)
|
||||
filter_count = IntegerField('FilterCount', 0)
|
||||
sent_count = IntegerField('SentCount', 0)
|
||||
error_count = IntegerField('ErrorCount', 0)
|
||||
|
||||
|
||||
@register_event('templatesendjobfinish')
|
||||
class TemplateSendJobFinishEvent(BaseEvent):
|
||||
"""
|
||||
模板消息任务完成事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/17/304c1885ea66dbedf7dc170d84999a9d.html
|
||||
"""
|
||||
id = IntegerField('MsgID')
|
||||
event = 'templatesendjobfinish'
|
||||
status = StringField('Status')
|
||||
|
||||
|
||||
class BaseScanCodeEvent(BaseEvent):
|
||||
key = StringField('EventKey')
|
||||
scan_code_info = BaseField('ScanCodeInfo', {})
|
||||
|
||||
@property
|
||||
def scan_type(self):
|
||||
return self.scan_code_info['ScanType']
|
||||
|
||||
@property
|
||||
def scan_result(self):
|
||||
return self.scan_code_info['ScanResult']
|
||||
|
||||
|
||||
@register_event('scancode_push')
|
||||
class ScanCodePushEvent(BaseScanCodeEvent):
|
||||
"""
|
||||
扫码推事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/981d772286d10d153a3dc4286c1ee5b5.html
|
||||
"""
|
||||
event = 'scancode_push'
|
||||
|
||||
|
||||
@register_event('scancode_waitmsg')
|
||||
class ScanCodeWaitMsgEvent(BaseScanCodeEvent):
|
||||
"""
|
||||
扫码推事件且弹出“消息接收中”提示框的事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/981d772286d10d153a3dc4286c1ee5b5.html
|
||||
"""
|
||||
event = 'scancode_waitmsg'
|
||||
|
||||
|
||||
class BasePictureEvent(BaseEvent):
|
||||
key = StringField('EventKey')
|
||||
pictures_info = BaseField('SendPicsInfo', {})
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
return int(self.pictures_info['Count'])
|
||||
|
||||
@property
|
||||
def pictures(self):
|
||||
if self.pictures_info['PicList']:
|
||||
items = self.pictures_info['PicList']['item']
|
||||
if self.count > 1:
|
||||
return items
|
||||
return [items]
|
||||
return []
|
||||
|
||||
|
||||
@register_event('pic_sysphoto')
|
||||
class PicSysPhotoEvent(BasePictureEvent):
|
||||
"""
|
||||
弹出系统拍照发图的事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/981d772286d10d153a3dc4286c1ee5b5.html
|
||||
"""
|
||||
event = 'pic_sysphoto'
|
||||
|
||||
|
||||
@register_event('pic_photo_or_album')
|
||||
class PicPhotoOrAlbumEvent(BasePictureEvent):
|
||||
"""
|
||||
弹出拍照或者相册发图的事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/981d772286d10d153a3dc4286c1ee5b5.html
|
||||
"""
|
||||
event = 'pic_photo_or_album'
|
||||
|
||||
|
||||
@register_event('pic_weixin')
|
||||
class PicWeChatEvent(BasePictureEvent):
|
||||
"""
|
||||
弹出微信相册发图器的事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/981d772286d10d153a3dc4286c1ee5b5.html
|
||||
"""
|
||||
event = 'pic_weixin'
|
||||
|
||||
|
||||
@register_event('location_select')
|
||||
class LocationSelectEvent(BaseEvent):
|
||||
"""
|
||||
弹出地理位置选择器的事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/981d772286d10d153a3dc4286c1ee5b5.html
|
||||
"""
|
||||
event = 'location_select'
|
||||
key = StringField('EventKey')
|
||||
location_info = BaseField('SendLocationInfo', {})
|
||||
|
||||
@property
|
||||
def location_x(self):
|
||||
return self.location_info['Location_X']
|
||||
|
||||
@property
|
||||
def location_y(self):
|
||||
return self.location_info['Location_Y']
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return self.location_x, self.location_y
|
||||
|
||||
@property
|
||||
def scale(self):
|
||||
return self.location_info['Scale']
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
return self.location_info['Label']
|
||||
|
||||
@property
|
||||
def poiname(self):
|
||||
return self.location_info['Poiname']
|
||||
|
||||
|
||||
@register_event('card_pass_check')
|
||||
class CardPassCheckEvent(BaseEvent):
|
||||
event = 'card_pass_check'
|
||||
card_id = StringField('CardId')
|
||||
|
||||
|
||||
@register_event('card_not_pass_check')
|
||||
class CardNotPassCheckEvent(BaseEvent):
|
||||
event = 'card_not_pass_check'
|
||||
card_id = StringField('CardId')
|
||||
|
||||
|
||||
@register_event('user_get_card')
|
||||
class UserGetCardEvent(BaseEvent):
|
||||
"""
|
||||
领取事件推送
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/16/28b34ee91675a04cb24853768debded4.html#.E9.A2.86.E5.8F.96.E4.BA.8B.E4.BB.B6.E6.8E.A8.E9.80.81
|
||||
"""
|
||||
event = 'user_get_card'
|
||||
card_id = StringField('CardId')
|
||||
is_given_by_friend = IntegerField('IsGiveByFriend')
|
||||
friend = StringField('FriendUserName')
|
||||
code = StringField('UserCardCode')
|
||||
old_code = StringField('OldUserCardCode')
|
||||
outer_id = StringField('OuterId')
|
||||
|
||||
|
||||
@register_event('user_del_card')
|
||||
class UserDeleteCardEvent(BaseEvent):
|
||||
"""
|
||||
卡券删除事件推送
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/16/28b34ee91675a04cb24853768debded4.html#.E5.88.A0.E9.99.A4.E4.BA.8B.E4.BB.B6.E6.8E.A8.E9.80.81
|
||||
"""
|
||||
event = 'user_del_card'
|
||||
card_id = StringField('CardId')
|
||||
code = StringField('UserCardCode')
|
||||
|
||||
|
||||
@register_event('user_consume_card')
|
||||
class UserConsumeCardEvent(BaseEvent):
|
||||
"""
|
||||
卡券核销事件推送
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/16/28b34ee91675a04cb24853768debded4.html#.E6.A0.B8.E9.94.80.E4.BA.8B.E4.BB.B6.E6.8E.A8.E9.80.81
|
||||
"""
|
||||
event = 'user_consume_card'
|
||||
card_id = StringField('CardId')
|
||||
code = StringField('UserCardCode')
|
||||
consume_source = StringField('ConsumeSource')
|
||||
location_id = StringField('LocationId')
|
||||
staff = StringField('StaffOpenId')
|
||||
|
||||
|
||||
@register_event('merchant_order')
|
||||
class MerchantOrderEvent(BaseEvent):
|
||||
event = 'merchant_order'
|
||||
order_id = StringField('OrderId')
|
||||
order_status = IntegerField('OrderStatus')
|
||||
product_id = StringField('ProductId')
|
||||
sku_info = StringField('SkuInfo')
|
||||
|
||||
|
||||
@register_event('kf_create_session')
|
||||
class KfCreateSessionEvent(BaseEvent):
|
||||
event = 'kf_create_session'
|
||||
account = StringField('KfAccount')
|
||||
|
||||
|
||||
@register_event('kf_close_session')
|
||||
class KfCloseSessionEvent(BaseEvent):
|
||||
event = 'kf_close_session'
|
||||
account = StringField('KfAccount')
|
||||
|
||||
|
||||
@register_event('kf_switch_session')
|
||||
class KfSwitchSessionEvent(BaseEvent):
|
||||
event = 'kf_switch_session'
|
||||
from_account = StringField('FromKfAccount')
|
||||
to_account = StringField('ToKfAccount')
|
||||
|
||||
|
||||
@register_event('device_text')
|
||||
class DeviceTextEvent(BaseEvent):
|
||||
event = 'device_text'
|
||||
device_type = StringField('DeviceType')
|
||||
device_id = StringField('DeviceID')
|
||||
session_id = StringField('SessionID')
|
||||
content = Base64DecodeField('Content')
|
||||
|
||||
|
||||
@register_event('device_bind')
|
||||
class DeviceBindEvent(BaseEvent):
|
||||
event = 'bind'
|
||||
device_type = StringField('DeviceType')
|
||||
device_id = StringField('DeviceID')
|
||||
session_id = StringField('SessionID')
|
||||
content = Base64DecodeField('Content')
|
||||
open_id = StringField('OpenID')
|
||||
|
||||
|
||||
@register_event('device_unbind')
|
||||
class DeviceUnbindEvent(BaseEvent):
|
||||
event = 'unbind'
|
||||
device_type = StringField('DeviceType')
|
||||
device_id = StringField('DeviceID')
|
||||
session_id = StringField('SessionID')
|
||||
content = Base64DecodeField('Content')
|
||||
open_id = StringField('OpenID')
|
||||
|
||||
|
||||
@register_event('device_subscribe_status')
|
||||
class DeviceSubscribeStatusEvent(BaseEvent):
|
||||
event = 'subscribe_status'
|
||||
device_type = StringField('DeviceType')
|
||||
device_id = StringField('DeviceID')
|
||||
open_id = StringField('OpenID')
|
||||
op_type = IntegerField('OpType')
|
||||
|
||||
|
||||
@register_event('device_unsubscribe_status')
|
||||
class DeviceUnsubscribeStatusEvent(BaseEvent):
|
||||
event = 'subscribe_status'
|
||||
device_type = StringField('DeviceType')
|
||||
device_id = StringField('DeviceID')
|
||||
open_id = StringField('OpenID')
|
||||
op_type = IntegerField('OpType')
|
||||
|
||||
|
||||
@register_event('shakearoundusershake')
|
||||
class ShakearoundUserShakeEvent(BaseEvent):
|
||||
event = 'shakearound_user_shake'
|
||||
_chosen_beacon = BaseField('ChosenBeacon', {})
|
||||
_around_beacons = BaseField('AroundBeacons', {})
|
||||
|
||||
@property
|
||||
def chosen_beacon(self):
|
||||
beacon = self._chosen_beacon
|
||||
if not beacon:
|
||||
return {}
|
||||
return {
|
||||
'uuid': beacon['Uuid'],
|
||||
'major': beacon['Major'],
|
||||
'minor': beacon['Minor'],
|
||||
'distance': float(beacon['Distance']),
|
||||
}
|
||||
|
||||
@property
|
||||
def around_beacons(self):
|
||||
beacons = self._around_beacons
|
||||
if not beacons:
|
||||
return []
|
||||
|
||||
ret = []
|
||||
for beacon in beacons['AroundBeacon']:
|
||||
ret.append({
|
||||
'uuid': beacon['Uuid'],
|
||||
'major': beacon['Major'],
|
||||
'minor': beacon['Minor'],
|
||||
'distance': float(beacon['Distance']),
|
||||
})
|
||||
return ret
|
||||
|
||||
|
||||
@register_event('poi_check_notify')
|
||||
class PoiCheckNotifyEvent(BaseEvent):
|
||||
event = 'poi_check_notify'
|
||||
poi_id = StringField('PoiId')
|
||||
uniq_id = StringField('UniqId')
|
||||
result = StringField('Result')
|
||||
message = StringField('Msg')
|
||||
|
||||
|
||||
@register_event('wificonnected')
|
||||
class WiFiConnectedEvent(BaseEvent):
|
||||
event = 'wificconnected'
|
||||
connect_time = IntegerField('ConnectTime')
|
||||
expire_time = IntegerField('ExpireTime')
|
||||
vendor_id = StringField('VendorId')
|
||||
shop_id = StringField('PlaceId')
|
||||
bssid = StringField('DeviceNo')
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# 微信认证事件推送
|
||||
# ============================================================================
|
||||
@register_event('qualification_verify_success')
|
||||
class QualificationVerifySuccessEvent(BaseEvent):
|
||||
"""
|
||||
资质认证成功事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/1/7f81dec16b801b34629091094c099439.html
|
||||
"""
|
||||
event = 'qualification_verify_success'
|
||||
expired_time = DateTimeField('ExpiredTime')
|
||||
|
||||
|
||||
@register_event('qualification_verify_fail')
|
||||
class QualificationVerifyFailEvent(BaseEvent):
|
||||
"""
|
||||
资质认证失败事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/1/7f81dec16b801b34629091094c099439.html
|
||||
"""
|
||||
event = 'qualification_verify_fail'
|
||||
fail_time = DateTimeField('FailTime')
|
||||
fail_reason = StringField('FailReason')
|
||||
|
||||
|
||||
@register_event('naming_verify_success')
|
||||
class NamingVerifySuccessEvent(BaseEvent):
|
||||
"""
|
||||
名称认证成功事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/1/7f81dec16b801b34629091094c099439.html
|
||||
"""
|
||||
event = 'naming_verify_success'
|
||||
expired_time = DateTimeField('ExpiredTime')
|
||||
|
||||
|
||||
@register_event('naming_verify_fail')
|
||||
class NamingVerifyFailEvent(BaseEvent):
|
||||
"""
|
||||
名称认证失败事件
|
||||
|
||||
客户端不打勾,但仍有接口权限。详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/1/7f81dec16b801b34629091094c099439.html
|
||||
"""
|
||||
event = 'naming_verify_fail'
|
||||
fail_time = DateTimeField('FailTime')
|
||||
fail_reason = StringField('FailReason')
|
||||
|
||||
|
||||
@register_event('annual_renew')
|
||||
class AnnualRenewEvent(BaseEvent):
|
||||
"""
|
||||
年审通知事件
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/1/7f81dec16b801b34629091094c099439.html
|
||||
"""
|
||||
event = 'annual_renew'
|
||||
expired_time = DateTimeField('ExpiredTime')
|
||||
|
||||
|
||||
@register_event('verify_expired')
|
||||
class VerifyExpiredEvent(BaseEvent):
|
||||
"""
|
||||
认证过期失效通知
|
||||
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/1/7f81dec16b801b34629091094c099439.html
|
||||
"""
|
||||
event = 'verify_expired'
|
||||
expired_time = DateTimeField('ExpiredTime')
|
||||
|
||||
|
||||
@register_event('user_scan_product')
|
||||
class UserScanProductEvent(BaseEvent):
|
||||
"""
|
||||
打开商品主页事件
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/f4109a5e44b4bfbc7eb1337eb739f3e3.html
|
||||
"""
|
||||
event = 'user_scan_product'
|
||||
standard = StringField('KeyStandard')
|
||||
key = StringField('KeyStr')
|
||||
country = StringField('Country')
|
||||
province = StringField('Province')
|
||||
city = StringField('City')
|
||||
sex = IntegerField('Sex')
|
||||
scene = IntegerField('Scene')
|
||||
|
||||
|
||||
@register_event('user_scan_product_enter_session')
|
||||
class UserScanProductEnterSessionEvent(BaseEvent):
|
||||
"""
|
||||
进入公众号事件
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/f4109a5e44b4bfbc7eb1337eb739f3e3.html
|
||||
"""
|
||||
event = 'user_scan_product_enter_session'
|
||||
standard = StringField('KeyStandard')
|
||||
key = StringField('KeyStr')
|
||||
|
||||
|
||||
@register_event('user_scan_product_async')
|
||||
class UserScanProductAsyncEvent(BaseEvent):
|
||||
"""
|
||||
地理位置信息异步推送事件
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/f4109a5e44b4bfbc7eb1337eb739f3e3.html
|
||||
"""
|
||||
event = 'user_scan_product_async'
|
||||
standard = StringField('KeyStandard')
|
||||
key = StringField('KeyStr')
|
||||
region_code = StringField('RegionCode')
|
||||
|
||||
|
||||
@register_event('user_scan_product_verify_action')
|
||||
class UserScanProductVerifyActionEvent(BaseEvent):
|
||||
"""
|
||||
商品审核结果事件
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/f4109a5e44b4bfbc7eb1337eb739f3e3.html
|
||||
"""
|
||||
event = 'user_scan_product_verify_action'
|
||||
standard = StringField('KeyStandard')
|
||||
key = StringField('KeyStr')
|
||||
result = StringField('Result')
|
||||
reason = StringField('ReasonMsg')
|
||||
|
||||
|
||||
@register_event('subscribe_scan_product')
|
||||
class SubscribeScanProductEvent(BaseEvent):
|
||||
"""
|
||||
用户在商品主页中关注公众号事件
|
||||
|
||||
详情请参考
|
||||
http://mp.weixin.qq.com/wiki/15/f4109a5e44b4bfbc7eb1337eb739f3e3.html
|
||||
"""
|
||||
event = 'subscribe_scan_product'
|
||||
event_key = StringField('EventKey')
|
||||
|
||||
@property
|
||||
def scene(self):
|
||||
return self.event_key.split('|', 1)[0]
|
||||
|
||||
@property
|
||||
def standard(self):
|
||||
return self.event_key.split('|')[1]
|
||||
|
||||
@property
|
||||
def key(self):
|
||||
return self.event_key.split('|')[2]
|
||||
130
sg_wechat_enterprise/we_api/exceptions.py
Normal file
130
sg_wechat_enterprise/we_api/exceptions.py
Normal file
@@ -0,0 +1,130 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.exceptions
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Basic exceptions definition.
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import six
|
||||
|
||||
from wechatpy.utils import to_binary, to_text
|
||||
|
||||
|
||||
class WeChatException(Exception):
|
||||
"""Base exception for wechatpy"""
|
||||
|
||||
def __init__(self, errcode, errmsg):
|
||||
"""
|
||||
:param errcode: Error code
|
||||
:param errmsg: Error message
|
||||
"""
|
||||
self.errcode = errcode
|
||||
self.errmsg = errmsg
|
||||
|
||||
def __str__(self):
|
||||
if six.PY2:
|
||||
return to_binary('Error code: {code}, message: {msg}'.format(
|
||||
code=self.errcode,
|
||||
msg=self.errmsg
|
||||
))
|
||||
else:
|
||||
return to_text('Error code: {code}, message: {msg}'.format(
|
||||
code=self.errcode,
|
||||
msg=self.errmsg
|
||||
))
|
||||
|
||||
def __repr__(self):
|
||||
_repr = '{klass}({code}, {msg}'.format(
|
||||
klass=self.__class__.__name__,
|
||||
code=self.errcode,
|
||||
msg=self.errmsg
|
||||
)
|
||||
if six.PY2:
|
||||
return to_binary(_repr)
|
||||
else:
|
||||
return to_text(_repr)
|
||||
|
||||
|
||||
class WeChatClientException(WeChatException):
|
||||
"""WeChat API client exception class"""
|
||||
def __init__(self, errcode, errmsg, client=None,
|
||||
request=None, response=None):
|
||||
super(WeChatClientException, self).__init__(errcode, errmsg)
|
||||
self.client = client
|
||||
self.request = request
|
||||
self.response = response
|
||||
|
||||
|
||||
class InvalidSignatureException(WeChatException):
|
||||
"""Invalid signature exception class"""
|
||||
|
||||
def __init__(self, errcode=-40001, errmsg='Invalid signature'):
|
||||
super(InvalidSignatureException, self).__init__(errcode, errmsg)
|
||||
|
||||
|
||||
class APILimitedException(WeChatClientException):
|
||||
"""WeChat API call limited exception class"""
|
||||
pass
|
||||
|
||||
|
||||
class InvalidAppIdException(WeChatException):
|
||||
"""Invalid app_id exception class"""
|
||||
|
||||
def __init__(self, errcode=-40005, errmsg='Invalid AppId'):
|
||||
super(InvalidAppIdException, self).__init__(errcode, errmsg)
|
||||
|
||||
|
||||
class WeChatOAuthException(WeChatClientException):
|
||||
"""WeChat OAuth API exception class"""
|
||||
pass
|
||||
|
||||
|
||||
class WeChatPayException(WeChatClientException):
|
||||
"""WeChat Pay API exception class"""
|
||||
def __init__(self, return_code, result_code=None, return_msg=None,
|
||||
errcode=None, errmsg=None, client=None,
|
||||
request=None, response=None):
|
||||
"""
|
||||
:param return_code: 返回状态码
|
||||
:param result_code: 业务结果
|
||||
:param return_msg: 返回信息
|
||||
:param errcode: 错误代码
|
||||
:param errmsg: 错误代码描述
|
||||
"""
|
||||
super(WeChatPayException, self).__init__(
|
||||
errcode,
|
||||
errmsg,
|
||||
client,
|
||||
request,
|
||||
response
|
||||
)
|
||||
self.return_code = return_code
|
||||
self.result_code = result_code
|
||||
self.return_msg = return_msg
|
||||
|
||||
def __str__(self):
|
||||
if six.PY2:
|
||||
return to_binary('Error code: {code}, message: {msg}'.format(
|
||||
code=self.return_code,
|
||||
msg=self.return_msg
|
||||
))
|
||||
else:
|
||||
return to_text('Error code: {code}, message: {msg}'.format(
|
||||
code=self.return_code,
|
||||
msg=self.return_msg
|
||||
))
|
||||
|
||||
def __repr__(self):
|
||||
_repr = '{klass}({code}, {msg})'.format(
|
||||
klass=self.__class__.__name__,
|
||||
code=self.return_code,
|
||||
msg=self.return_msg
|
||||
)
|
||||
if six.PY2:
|
||||
return to_binary(_repr)
|
||||
else:
|
||||
return to_text(_repr)
|
||||
237
sg_wechat_enterprise/we_api/fields.py
Normal file
237
sg_wechat_enterprise/we_api/fields.py
Normal file
@@ -0,0 +1,237 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.fields
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
This module defines some useful field types for parse WeChat messages
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
from datetime import datetime
|
||||
import base64
|
||||
import copy
|
||||
|
||||
import six
|
||||
|
||||
from wechatpy.utils import to_text, to_binary, ObjectDict, timezone
|
||||
|
||||
default_timezone = timezone('Asia/Shanghai')
|
||||
|
||||
|
||||
class FieldDescriptor(object):
|
||||
def __init__(self, field):
|
||||
self.field = field
|
||||
self.attr_name = field.name
|
||||
|
||||
def __get__(self, instance, instance_type=None):
|
||||
if instance is not None:
|
||||
value = instance._data.get(self.attr_name)
|
||||
if value is None:
|
||||
value = copy.deepcopy(self.field.default)
|
||||
instance._data[self.attr_name] = value
|
||||
if isinstance(value, dict):
|
||||
value = ObjectDict(value)
|
||||
if value and not isinstance(value, (dict, list, tuple)) and \
|
||||
six.callable(self.field.converter):
|
||||
value = self.field.converter(value)
|
||||
return value
|
||||
return self.field
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance._data[self.attr_name] = value
|
||||
|
||||
|
||||
class BaseField(object):
|
||||
converter = None
|
||||
|
||||
def __init__(self, name, default=None):
|
||||
self.name = name
|
||||
self.default = default
|
||||
|
||||
def to_xml(self, value):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __repr__(self):
|
||||
_repr = '{klass}({name})'.format(
|
||||
klass=self.__class__.__name__,
|
||||
name=repr(self.name)
|
||||
)
|
||||
if six.PY2:
|
||||
return to_binary(_repr)
|
||||
else:
|
||||
return to_text(_repr)
|
||||
|
||||
def add_to_class(self, klass, name):
|
||||
self.klass = klass
|
||||
klass._fields[name] = self
|
||||
setattr(klass, name, FieldDescriptor(self))
|
||||
|
||||
|
||||
class StringField(BaseField):
|
||||
def __to_text(self, value):
|
||||
return to_text(value)
|
||||
|
||||
converter = __to_text
|
||||
|
||||
def to_xml(self, value):
|
||||
value = self.converter(value)
|
||||
tpl = '<{name}><![CDATA[{value}]]></{name}>'
|
||||
return tpl.format(name=self.name, value=value)
|
||||
|
||||
|
||||
class IntegerField(BaseField):
|
||||
converter = int
|
||||
|
||||
def to_xml(self, value):
|
||||
value = self.converter(value) if value is not None else self.default
|
||||
tpl = '<{name}>{value}</{name}>'
|
||||
return tpl.format(name=self.name, value=value)
|
||||
|
||||
|
||||
class DateTimeField(BaseField):
|
||||
def __converter(self, value):
|
||||
v = int(value)
|
||||
return datetime.fromtimestamp(v, tz=default_timezone)
|
||||
|
||||
converter = __converter
|
||||
|
||||
def to_xml(self, value):
|
||||
value = time.mktime(datetime.timetuple(value))
|
||||
value = int(value)
|
||||
tpl = '<{name}>{value}</{name}>'
|
||||
return tpl.format(name=self.name, value=value)
|
||||
|
||||
|
||||
class FloatField(BaseField):
|
||||
converter = float
|
||||
|
||||
def to_xml(self, value):
|
||||
value = self.converter(value) if value is not None else self.default
|
||||
tpl = '<{name}>{value}</{name}>'
|
||||
return tpl.format(name=self.name, value=value)
|
||||
|
||||
|
||||
class ImageField(StringField):
|
||||
def to_xml(self, value):
|
||||
value = self.converter(value)
|
||||
tpl = """<Image>
|
||||
<MediaId><![CDATA[{value}]]></MediaId>
|
||||
</Image>"""
|
||||
return tpl.format(value=value)
|
||||
|
||||
|
||||
class VoiceField(StringField):
|
||||
def to_xml(self, value):
|
||||
value = self.converter(value)
|
||||
tpl = """<Voice>
|
||||
<MediaId><![CDATA[{value}]]></MediaId>
|
||||
</Voice>"""
|
||||
return tpl.format(value=value)
|
||||
|
||||
|
||||
class VideoField(StringField):
|
||||
def to_xml(self, value):
|
||||
media_id = self.converter(value['media_id'])
|
||||
if 'title' in value:
|
||||
title = self.converter(value['title'])
|
||||
if 'description' in value:
|
||||
description = self.converter(value['description'])
|
||||
tpl = """<Video>
|
||||
<MediaId><![CDATA[{media_id}]]></MediaId>
|
||||
<Title><![CDATA[{title}]]></Title>
|
||||
<Description><![CDATA[{description}]]></Description>
|
||||
</Video>"""
|
||||
return tpl.format(
|
||||
media_id=media_id,
|
||||
title=title,
|
||||
description=description
|
||||
)
|
||||
|
||||
|
||||
class MusicField(StringField):
|
||||
def to_xml(self, value):
|
||||
thumb_media_id = self.converter(value['thumb_media_id'])
|
||||
if 'title' in value:
|
||||
title = self.converter(value['title'])
|
||||
if 'description' in value:
|
||||
description = self.converter(value['description'])
|
||||
if 'music_url' in value:
|
||||
music_url = self.converter(value['music_url'])
|
||||
if 'hq_music_url' in value:
|
||||
hq_music_url = self.converter(value['hq_music_url'])
|
||||
tpl = """<Music>
|
||||
<ThumbMediaId><![CDATA[{thumb_media_id}]]></ThumbMediaId>
|
||||
<Title><![CDATA[{title}]]></Title>
|
||||
<Description><![CDATA[{description}]]></Description>
|
||||
<MusicUrl><![CDATA[{music_url}]]></MusicUrl>
|
||||
<HQMusicUrl><![CDATA[{hq_music_url}]]></HQMusicUrl>
|
||||
</Music>"""
|
||||
return tpl.format(
|
||||
thumb_media_id=thumb_media_id,
|
||||
title=title,
|
||||
description=description,
|
||||
music_url=music_url,
|
||||
hq_music_url=hq_music_url
|
||||
)
|
||||
|
||||
|
||||
class ArticlesField(StringField):
|
||||
def to_xml(self, articles):
|
||||
article_count = len(articles)
|
||||
items = []
|
||||
for article in articles:
|
||||
title = self.converter(article.get('title', ''))
|
||||
description = self.converter(article.get('description', ''))
|
||||
image = self.converter(article.get('image', ''))
|
||||
url = self.converter(article.get('url', ''))
|
||||
item_tpl = """<item>
|
||||
<Title><![CDATA[{title}]]></Title>
|
||||
<Description><![CDATA[{description}]]></Description>
|
||||
<PicUrl><![CDATA[{image}]]></PicUrl>
|
||||
<Url><![CDATA[{url}]]></Url>
|
||||
</item>"""
|
||||
item = item_tpl.format(
|
||||
title=title,
|
||||
description=description,
|
||||
image=image,
|
||||
url=url
|
||||
)
|
||||
items.append(item)
|
||||
items_str = '\n'.join(items)
|
||||
tpl = """<ArticleCount>{article_count}</ArticleCount>
|
||||
<Articles>{items}</Articles>"""
|
||||
return tpl.format(
|
||||
article_count=article_count,
|
||||
items=items_str
|
||||
)
|
||||
|
||||
|
||||
class Base64EncodeField(StringField):
|
||||
def __base64_encode(self, text):
|
||||
return to_text(base64.b64encode(to_binary(text)))
|
||||
|
||||
converter = __base64_encode
|
||||
|
||||
|
||||
class Base64DecodeField(StringField):
|
||||
def __base64_decode(self, text):
|
||||
return to_text(base64.b64decode(to_binary(text)))
|
||||
|
||||
converter = __base64_decode
|
||||
|
||||
|
||||
class HardwareField(StringField):
|
||||
def to_xml(self, value=None):
|
||||
value = value or {'xml': 'myrank', 'action': 'ranklist'}
|
||||
tpl = """<{name}>
|
||||
<MessageView><![CDATA[{xml}]]></MessageView>
|
||||
<MessageAction><![CDATA[{action}]]></MessageAction>
|
||||
</{name}>"""
|
||||
return tpl.format(
|
||||
name=self.name,
|
||||
view=value.get('xml'),
|
||||
action=value.get('action')
|
||||
)
|
||||
173
sg_wechat_enterprise/we_api/messages.py
Normal file
173
sg_wechat_enterprise/we_api/messages.py
Normal file
@@ -0,0 +1,173 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.messages
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
This module defines all the messages you can get from WeChat server
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import copy
|
||||
import six
|
||||
|
||||
from wechatpy.fields import (
|
||||
BaseField,
|
||||
StringField,
|
||||
IntegerField,
|
||||
DateTimeField,
|
||||
FieldDescriptor
|
||||
)
|
||||
from wechatpy.utils import to_text, to_binary
|
||||
|
||||
|
||||
MESSAGE_TYPES = {}
|
||||
|
||||
|
||||
def register_message(msg_type):
|
||||
def register(cls):
|
||||
MESSAGE_TYPES[msg_type] = cls
|
||||
return cls
|
||||
return register
|
||||
|
||||
|
||||
class MessageMetaClass(type):
|
||||
"""Metaclass for all messages"""
|
||||
def __new__(cls, name, bases, attrs):
|
||||
for b in bases:
|
||||
if not hasattr(b, '_fields'):
|
||||
continue
|
||||
|
||||
for k, v in b.__dict__.items():
|
||||
if k in attrs:
|
||||
continue
|
||||
if isinstance(v, FieldDescriptor):
|
||||
attrs[k] = copy.deepcopy(v.field)
|
||||
|
||||
cls = super(MessageMetaClass, cls).__new__(cls, name, bases, attrs)
|
||||
cls._fields = {}
|
||||
|
||||
for name, field in cls.__dict__.items():
|
||||
if isinstance(field, BaseField):
|
||||
field.add_to_class(cls, name)
|
||||
return cls
|
||||
|
||||
|
||||
class BaseMessage(six.with_metaclass(MessageMetaClass)):
|
||||
"""Base class for all messages and events"""
|
||||
type = 'unknown'
|
||||
id = IntegerField('MsgId', 0)
|
||||
source = StringField('FromUserName')
|
||||
target = StringField('ToUserName')
|
||||
create_time = DateTimeField('CreateTime')
|
||||
time = IntegerField('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)
|
||||
|
||||
|
||||
@register_message('text')
|
||||
class TextMessage(BaseMessage):
|
||||
"""
|
||||
文本消息
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html
|
||||
"""
|
||||
type = 'text'
|
||||
content = StringField('Content')
|
||||
|
||||
|
||||
@register_message('image')
|
||||
class ImageMessage(BaseMessage):
|
||||
"""
|
||||
图片消息
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html
|
||||
"""
|
||||
type = 'image'
|
||||
media_id = StringField('MediaId')
|
||||
image = StringField('PicUrl')
|
||||
|
||||
|
||||
@register_message('voice')
|
||||
class VoiceMessage(BaseMessage):
|
||||
"""
|
||||
语音消息
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html
|
||||
"""
|
||||
type = 'voice'
|
||||
media_id = StringField('MediaId')
|
||||
format = StringField('Format')
|
||||
recognition = StringField('Recognition')
|
||||
|
||||
|
||||
@register_message('shortvideo')
|
||||
class ShortVideoMessage(BaseMessage):
|
||||
"""
|
||||
短视频消息
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html
|
||||
"""
|
||||
type = 'shortvideo'
|
||||
media_id = StringField('MediaId')
|
||||
thumb_media_id = StringField('ThumbMediaId')
|
||||
|
||||
|
||||
@register_message('video')
|
||||
class VideoMessage(BaseMessage):
|
||||
"""
|
||||
视频消息
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html
|
||||
"""
|
||||
type = 'video'
|
||||
media_id = StringField('MediaId')
|
||||
thumb_media_id = StringField('ThumbMediaId')
|
||||
|
||||
|
||||
@register_message('location')
|
||||
class LocationMessage(BaseMessage):
|
||||
"""
|
||||
地理位置消息
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html
|
||||
"""
|
||||
type = 'location'
|
||||
location_x = StringField('Location_X')
|
||||
location_y = StringField('Location_Y')
|
||||
scale = StringField('Scale')
|
||||
label = StringField('Label')
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
return self.location_x, self.location_y
|
||||
|
||||
|
||||
@register_message('link')
|
||||
class LinkMessage(BaseMessage):
|
||||
"""
|
||||
链接消息
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/10/79502792eef98d6e0c6e1739da387346.html
|
||||
"""
|
||||
type = 'link'
|
||||
title = StringField('Title')
|
||||
description = StringField('Description')
|
||||
url = StringField('Url')
|
||||
|
||||
|
||||
class UnknownMessage(BaseMessage):
|
||||
"""未知消息类型"""
|
||||
pass
|
||||
219
sg_wechat_enterprise/we_api/oauth/__init__.py
Normal file
219
sg_wechat_enterprise/we_api/oauth/__init__.py
Normal file
@@ -0,0 +1,219 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.oauth
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides OAuth2 library for WeChat
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
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 requests
|
||||
from six.moves.urllib.parse import quote
|
||||
# from urllib import quote
|
||||
from wechatpy.utils import json
|
||||
from wechatpy.exceptions import WeChatOAuthException
|
||||
|
||||
|
||||
class WeChatOAuth(object):
|
||||
"""微信公众平台 OAuth 网页授权 """
|
||||
|
||||
API_BASE_URL = 'https://api.weixin.qq.com/'
|
||||
OAUTH_BASE_URL = 'https://open.weixin.qq.com/connect/'
|
||||
|
||||
def __init__(self, app_id, secret, redirect_uri,
|
||||
scope='snsapi_base', state=''):
|
||||
"""
|
||||
|
||||
:param app_id: 微信公众号 app_id
|
||||
:param secret: 微信公众号 secret
|
||||
:param redirect_uri: OAuth2 redirect URI
|
||||
:param scope: 可选,微信公众号 OAuth2 scope,默认为 ``snsapi_base``
|
||||
:param state: 可选,微信公众号 OAuth2 state
|
||||
"""
|
||||
self.app_id = app_id
|
||||
self.secret = secret
|
||||
self.redirect_uri = redirect_uri
|
||||
self.scope = scope
|
||||
self.state = state
|
||||
|
||||
def _request(self, method, url_or_endpoint, **kwargs):
|
||||
if not url_or_endpoint.startswith(('http://', 'https://')):
|
||||
url = '{base}{endpoint}'.format(
|
||||
base=self.API_BASE_URL,
|
||||
endpoint=url_or_endpoint
|
||||
)
|
||||
else:
|
||||
url = url_or_endpoint
|
||||
|
||||
if isinstance(kwargs.get('data', ''), dict):
|
||||
body = json.dumps(kwargs['data'], ensure_ascii=False)
|
||||
body = body.encode('utf-8')
|
||||
kwargs['data'] = body
|
||||
|
||||
res = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
**kwargs
|
||||
)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except requests.RequestException as reqe:
|
||||
raise WeChatOAuthException(
|
||||
errcode=None,
|
||||
errmsg=None,
|
||||
client=self,
|
||||
request=reqe.request,
|
||||
response=reqe.response
|
||||
)
|
||||
result = json.loads(res.content.decode('utf-8', 'ignore'), strict=False)
|
||||
|
||||
if 'errcode' in result and result['errcode'] != 0:
|
||||
errcode = result['errcode']
|
||||
errmsg = result['errmsg']
|
||||
raise WeChatOAuthException(
|
||||
errcode,
|
||||
errmsg,
|
||||
client=self,
|
||||
request=res.request,
|
||||
response=res
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
def _get(self, url, **kwargs):
|
||||
return self._request(
|
||||
method='get',
|
||||
url_or_endpoint=url,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
@property
|
||||
def authorize_url(self):
|
||||
"""获取授权跳转地址
|
||||
|
||||
:return: URL 地址
|
||||
"""
|
||||
redirect_uri = quote(self.redirect_uri)
|
||||
url_list = [
|
||||
self.OAUTH_BASE_URL,
|
||||
'oauth2/authorize?appid=',
|
||||
self.app_id,
|
||||
'&redirect_uri=',
|
||||
redirect_uri,
|
||||
'&response_type=code&scope=',
|
||||
self.scope
|
||||
]
|
||||
if self.state:
|
||||
url_list.extend(['&state=', self.state])
|
||||
url_list.append('#wechat_redirect')
|
||||
return ''.join(url_list)
|
||||
|
||||
@property
|
||||
def qrconnect_url(self):
|
||||
"""生成扫码登录地址
|
||||
|
||||
:return: URL 地址
|
||||
"""
|
||||
redirect_uri = quote(self.redirect_uri)
|
||||
url_list = [
|
||||
self.OAUTH_BASE_URL,
|
||||
'qrconnect?appid=',
|
||||
self.app_id,
|
||||
'&redirect_uri=',
|
||||
redirect_uri,
|
||||
'&response_type=code&scope=',
|
||||
'snsapi_login' # scope
|
||||
]
|
||||
if self.state:
|
||||
url_list.extend(['&state=', self.state])
|
||||
url_list.append('#wechat_redirect')
|
||||
return ''.join(url_list)
|
||||
|
||||
def fetch_access_token(self, code):
|
||||
"""获取 access_token
|
||||
|
||||
:param code: 授权完成跳转回来后 URL 中的 code 参数
|
||||
:return: JSON 数据包 1
|
||||
"""
|
||||
res = self._get(
|
||||
'sns/oauth2/access_token',
|
||||
params={
|
||||
'appid': self.app_id,
|
||||
'secret': self.secret,
|
||||
'code': code,
|
||||
'grant_type': 'authorization_code'
|
||||
}
|
||||
)
|
||||
self.access_token = res['access_token']
|
||||
self.open_id = res['openid']
|
||||
self.refresh_token = res['refresh_token']
|
||||
self.expires_in = res['expires_in']
|
||||
return res
|
||||
|
||||
def refresh_access_token(self, refresh_token):
|
||||
"""刷新 access token
|
||||
|
||||
:param refresh_token: OAuth2 refresh token
|
||||
:return: JSON 数据包
|
||||
"""
|
||||
res = self._get(
|
||||
'sns/oauth2/refresh_token',
|
||||
params={
|
||||
'appid': self.app_id,
|
||||
'grant_type': 'refresh_token',
|
||||
'refresh_token': refresh_token
|
||||
}
|
||||
)
|
||||
self.access_token = res['access_token']
|
||||
self.open_id = res['openid']
|
||||
self.refresh_token = res['refresh_token']
|
||||
self.expires_in = res['expires_in']
|
||||
return res
|
||||
|
||||
def get_user_info(self, openid=None, access_token=None, lang='zh_CN'):
|
||||
"""获取用户信息
|
||||
|
||||
:param openid: 可选,微信 openid,默认获取当前授权用户信息
|
||||
:param access_token: 可选,access_token,默认使用当前授权用户的 access_token
|
||||
:param lang: 可选,语言偏好, 默认为 ``zh_CN``
|
||||
:return: JSON 数据包
|
||||
"""
|
||||
openid = openid or self.open_id
|
||||
access_token = access_token or self.access_token
|
||||
return self._get(
|
||||
'sns/userinfo',
|
||||
params={
|
||||
'access_token': access_token,
|
||||
'openid': openid,
|
||||
'lang': lang
|
||||
}
|
||||
)
|
||||
|
||||
def check_access_token(self, openid=None, access_token=None):
|
||||
"""检查 access_token 有效性
|
||||
|
||||
:param openid: 可选,微信 openid,默认获取当前授权用户信息
|
||||
:param access_token: 可选,access_token,默认使用当前授权用户的 access_token
|
||||
:return: 有效返回 True,否则 False
|
||||
"""
|
||||
openid = openid or self.open_id
|
||||
access_token = access_token or self.access_token
|
||||
res = self._get(
|
||||
'sns/auth',
|
||||
params={
|
||||
'access_token': access_token,
|
||||
'openid': openid
|
||||
}
|
||||
)
|
||||
if res['errcode'] == 0:
|
||||
return True
|
||||
return False
|
||||
47
sg_wechat_enterprise/we_api/parser.py
Normal file
47
sg_wechat_enterprise/we_api/parser.py
Normal file
@@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.parser
|
||||
~~~~~~~~~~~~~~~~
|
||||
This module provides functions for parsing WeChat messages
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import xmltodict
|
||||
|
||||
from wechatpy.messages import MESSAGE_TYPES, UnknownMessage
|
||||
from wechatpy.events import EVENT_TYPES
|
||||
from wechatpy.utils import to_text
|
||||
|
||||
|
||||
def parse_message(xml):
|
||||
"""
|
||||
解析微信服务器推送的 XML 消息
|
||||
|
||||
:param xml: XML 消息
|
||||
:return: 解析成功返回对应的消息或事件,否则返回 ``UnknownMessage``
|
||||
"""
|
||||
if not xml:
|
||||
return
|
||||
message = xmltodict.parse(to_text(xml))['xml']
|
||||
message_type = message['MsgType'].lower()
|
||||
if message_type in ('event', 'device_event'):
|
||||
event_type = message['Event'].lower()
|
||||
# special event type for device_event
|
||||
if message_type == 'device_event':
|
||||
event_type = 'device_{event}'.format(event=event_type)
|
||||
if event_type == 'subscribe' and message.get('EventKey'):
|
||||
event_key = message['EventKey']
|
||||
if event_key.startswith(('scanbarcode|', 'scanimage|')):
|
||||
event_type = 'subscribe_scan_product'
|
||||
message['Event'] = event_type
|
||||
else:
|
||||
# Scan to subscribe with scene id event
|
||||
event_type = 'subscribe_scan'
|
||||
message['Event'] = event_type
|
||||
message['EventKey'] = event_key.replace('qrscene_', '')
|
||||
message_class = EVENT_TYPES.get(event_type, UnknownMessage)
|
||||
else:
|
||||
message_class = MESSAGE_TYPES.get(message_type, UnknownMessage)
|
||||
return message_class(message)
|
||||
199
sg_wechat_enterprise/we_api/pay/__init__.py
Normal file
199
sg_wechat_enterprise/we_api/pay/__init__.py
Normal file
@@ -0,0 +1,199 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import sys
|
||||
import inspect
|
||||
import logging
|
||||
|
||||
import requests
|
||||
import xmltodict
|
||||
from xml.parsers.expat import ExpatError
|
||||
from optionaldict import optionaldict
|
||||
|
||||
from wechatpy.utils import random_string
|
||||
from wechatpy.exceptions import WeChatPayException, InvalidSignatureException
|
||||
from wechatpy.pay.utils import (
|
||||
calculate_signature, _check_signature, dict_to_xml
|
||||
)
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
from wechatpy.pay import api
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _is_api_endpoint(obj):
|
||||
return isinstance(obj, BaseWeChatPayAPI)
|
||||
|
||||
|
||||
class WeChatPay(object):
|
||||
"""
|
||||
微信红包接口
|
||||
|
||||
:param appid: 微信公众号 appid
|
||||
:param api_key: 商户 key
|
||||
:param mch_id: 商户号
|
||||
:param sub_mch_id: 可选,子商户号,受理模式下必填
|
||||
:param mch_cert: 必填,商户证书路径
|
||||
:param mch_key: 必填,商户证书私钥路径
|
||||
"""
|
||||
redpack = api.WeChatRedpack()
|
||||
"""红包接口"""
|
||||
transfer = api.WeChatTransfer()
|
||||
"""企业付款接口"""
|
||||
coupon = api.WeChatCoupon()
|
||||
"""代金券接口"""
|
||||
order = api.WeChatOrder()
|
||||
"""订单接口"""
|
||||
refund = api.WeChatRefund()
|
||||
"""退款接口"""
|
||||
micropay = api.WeChatMicroPay()
|
||||
"""刷卡支付接口"""
|
||||
tools = api.WeChatTools()
|
||||
"""工具类接口"""
|
||||
jsapi = api.WeChatJSAPI()
|
||||
|
||||
API_BASE_URL = 'https://api.mch.weixin.qq.com/'
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
self = super(WeChatPay, cls).__new__(cls)
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
import copy
|
||||
# Python 2.6 inspect.gemembers bug workaround
|
||||
# http://bugs.python.org/issue1785
|
||||
for name, _api in self.__class__.__dict__.items():
|
||||
if isinstance(_api, BaseWeChatPayAPI):
|
||||
_api = copy.deepcopy(_api)
|
||||
_api._client = 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, api_key, mch_id, sub_mch_id=None,
|
||||
mch_cert=None, mch_key=None):
|
||||
"""
|
||||
:param appid: 微信公众号 appid
|
||||
:param api_key: 商户 key
|
||||
:param mch_id: 商户号
|
||||
:param sub_mch_id: 可选,子商户号,受理模式下必填
|
||||
:param mch_cert: 商户证书路径
|
||||
:param mch_key: 商户证书私钥路径
|
||||
"""
|
||||
self.appid = appid
|
||||
self.api_key = api_key
|
||||
self.mch_id = mch_id
|
||||
self.sub_mch_id = sub_mch_id
|
||||
self.mch_cert = mch_cert
|
||||
self.mch_key = mch_key
|
||||
|
||||
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 isinstance(kwargs.get('data', ''), dict):
|
||||
data = optionaldict(kwargs['data'])
|
||||
if 'mchid' not in data:
|
||||
# Fuck Tencent
|
||||
data.setdefault('mch_id', self.mch_id)
|
||||
data.setdefault('sub_mch_id', self.sub_mch_id)
|
||||
data.setdefault('nonce_str', random_string(32))
|
||||
sign = calculate_signature(data, self.api_key)
|
||||
body = dict_to_xml(data, sign)
|
||||
body = body.encode('utf-8')
|
||||
kwargs['data'] = body
|
||||
|
||||
# 商户证书
|
||||
if self.mch_cert and self.mch_key:
|
||||
kwargs['cert'] = (self.mch_cert, self.mch_key)
|
||||
|
||||
res = requests.request(
|
||||
method=method,
|
||||
url=url,
|
||||
**kwargs
|
||||
)
|
||||
try:
|
||||
res.raise_for_status()
|
||||
except requests.RequestException as reqe:
|
||||
raise WeChatPayException(
|
||||
return_code=None,
|
||||
client=self,
|
||||
request=reqe.request,
|
||||
response=reqe.response
|
||||
)
|
||||
|
||||
return self._handle_result(res)
|
||||
|
||||
def _handle_result(self, res):
|
||||
res.encoding = 'utf-8'
|
||||
xml = res.text
|
||||
try:
|
||||
data = xmltodict.parse(xml)['xml']
|
||||
except (xmltodict.ParsingInterrupted, ExpatError):
|
||||
# 解析 XML 失败
|
||||
logger.debug('WeChat payment result xml parsing error', exc_info=True)
|
||||
return xml
|
||||
|
||||
return_code = data['return_code']
|
||||
return_msg = data.get('return_msg')
|
||||
result_code = data.get('result_code')
|
||||
errcode = data.get('err_code')
|
||||
errmsg = data.get('err_code_des')
|
||||
if return_code != 'SUCCESS' or result_code != 'SUCCESS':
|
||||
# 返回状态码不为成功
|
||||
raise WeChatPayException(
|
||||
return_code,
|
||||
result_code,
|
||||
return_msg,
|
||||
errcode,
|
||||
errmsg,
|
||||
client=self,
|
||||
request=res.request,
|
||||
response=res
|
||||
)
|
||||
return data
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
def check_signature(self, params):
|
||||
return _check_signature(params, self.api_key)
|
||||
|
||||
def parse_payment_result(self, xml):
|
||||
"""解析微信支付结果通知"""
|
||||
try:
|
||||
data = xmltodict.parse(xml)
|
||||
except (xmltodict.ParsingInterrupted, ExpatError):
|
||||
raise InvalidSignatureException()
|
||||
|
||||
if not data or 'xml' not in data:
|
||||
raise InvalidSignatureException()
|
||||
|
||||
data = data['xml']
|
||||
sign = data.pop('sign', None)
|
||||
real_sign = calculate_signature(data, self.api_key)
|
||||
if sign != real_sign:
|
||||
raise InvalidSignatureException()
|
||||
|
||||
data['sign'] = sign
|
||||
return data
|
||||
10
sg_wechat_enterprise/we_api/pay/api/__init__.py
Normal file
10
sg_wechat_enterprise/we_api/pay/api/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.pay.api.redpack import WeChatRedpack # NOQA
|
||||
from wechatpy.pay.api.transfer import WeChatTransfer # NOQA
|
||||
from wechatpy.pay.api.coupon import WeChatCoupon # NOQA
|
||||
from wechatpy.pay.api.order import WeChatOrder # NOQA
|
||||
from wechatpy.pay.api.refund import WeChatRefund # NOQA
|
||||
from wechatpy.pay.api.tools import WeChatTools # NOQA
|
||||
from wechatpy.pay.api.jsapi import WeChatJSAPI # NOQA
|
||||
from wechatpy.pay.api.micropay import WeChatMicroPay # NOQA
|
||||
82
sg_wechat_enterprise/we_api/pay/api/coupon.py
Normal file
82
sg_wechat_enterprise/we_api/pay/api/coupon.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
|
||||
|
||||
class WeChatCoupon(BaseWeChatPayAPI):
|
||||
|
||||
def send(self, user_id, stock_id, op_user_id=None, device_info=None,
|
||||
out_trade_no=None):
|
||||
"""
|
||||
发放代金券
|
||||
|
||||
:param user_id: 用户在公众号下的 openid
|
||||
:param stock_id: 代金券批次 ID
|
||||
:param op_user_id: 可选,操作员账号,默认为商户号
|
||||
:param device_info: 可选,微信支付分配的终端设备号
|
||||
:param out_trade_no: 可选,商户订单号,需保持唯一性,默认自动生成
|
||||
:return: 返回的结果信息
|
||||
"""
|
||||
if not out_trade_no:
|
||||
now = datetime.now()
|
||||
out_trade_no = '{0}{1}{2}'.format(
|
||||
self.mch_id,
|
||||
now.strftime('%Y%m%d%H%M%S'),
|
||||
random.randint(1000, 10000)
|
||||
)
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'coupon_stock_id': stock_id,
|
||||
'openid': user_id,
|
||||
'openid_count': 1,
|
||||
'partner_trade_no': out_trade_no,
|
||||
'op_user_id': op_user_id,
|
||||
'device_info': device_info,
|
||||
'version': '1.0',
|
||||
'type': 'XML',
|
||||
}
|
||||
return self._post('mmpaymkttransfers/send_coupon', data=data)
|
||||
|
||||
def query_stock(self, stock_id, op_user_id=None, device_info=None):
|
||||
"""
|
||||
查询代金券批次
|
||||
|
||||
:param stock_id: 代金券批次 ID
|
||||
:param op_user_id: 可选,操作员账号,默认为商户号
|
||||
:param device_info: 可选,微信支付分配的终端设备号
|
||||
:return: 返回的结果信息
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'coupon_stock_id': stock_id,
|
||||
'op_user_id': op_user_id,
|
||||
'device_info': device_info,
|
||||
'version': '1.0',
|
||||
'type': 'XML',
|
||||
}
|
||||
return self._post('mmpaymkttransfers/query_coupon_stock', data=data)
|
||||
|
||||
def query_coupon(self, coupon_id, user_id,
|
||||
op_user_id=None, device_info=None):
|
||||
"""
|
||||
查询代金券信息
|
||||
|
||||
:param coupon_id: 代金券 ID
|
||||
:param user_id: 用户在公众号下的 openid
|
||||
:param op_user_id: 可选,操作员账号,默认为商户号
|
||||
:param device_info: 可选,微信支付分配的终端设备号
|
||||
:return: 返回的结果信息
|
||||
"""
|
||||
data = {
|
||||
'coupon_id': coupon_id,
|
||||
'openid': user_id,
|
||||
'appid': self.appid,
|
||||
'op_user_id': op_user_id,
|
||||
'device_info': device_info,
|
||||
'version': '1.0',
|
||||
'type': 'XML',
|
||||
}
|
||||
return self._post('promotion/query_coupon', data=data)
|
||||
48
sg_wechat_enterprise/we_api/pay/api/jsapi.py
Normal file
48
sg_wechat_enterprise/we_api/pay/api/jsapi.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
|
||||
from wechatpy.utils import random_string, to_text
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
from wechatpy.pay.utils import calculate_signature
|
||||
|
||||
|
||||
class WeChatJSAPI(BaseWeChatPayAPI):
|
||||
|
||||
def get_jsapi_signature(self, prepay_id, timestamp=None, nonce_str=None):
|
||||
"""
|
||||
获取 JSAPI 签名
|
||||
|
||||
:param prepay_id: 统一下单接口返回的 prepay_id 参数值
|
||||
:param timestamp: 可选,时间戳,默认为当前时间戳
|
||||
:param nonce_str: 可选,随机字符串,默认自动生成
|
||||
:return: 签名
|
||||
"""
|
||||
data = {
|
||||
'appId': self.appid,
|
||||
'timeStamp': timestamp or to_text(int(time.time())),
|
||||
'nonceStr': nonce_str or random_string(32),
|
||||
'signType': 'MD5',
|
||||
'package': 'prepay_id={0}'.format(prepay_id),
|
||||
}
|
||||
return calculate_signature(data, self._client.api_key)
|
||||
|
||||
def get_jsapi_params(self, prepay_id, timestamp=None, nonce_str=None):
|
||||
"""
|
||||
获取 JSAPI 参数
|
||||
|
||||
:param prepay_id: 统一下单接口返回的 prepay_id 参数值
|
||||
:param timestamp: 可选,时间戳,默认为当前时间戳
|
||||
:param nonce_str: 可选,随机字符串,默认自动生成
|
||||
:return: 签名
|
||||
"""
|
||||
data = {
|
||||
'appId': self.appid,
|
||||
'timeStamp': timestamp or to_text(int(time.time())),
|
||||
'nonceStr': nonce_str or random_string(32),
|
||||
'signType': 'MD5',
|
||||
'package': 'prepay_id={0}'.format(prepay_id),
|
||||
}
|
||||
sign = calculate_signature(data, self._client.api_key)
|
||||
data['paySign'] = sign
|
||||
return data
|
||||
64
sg_wechat_enterprise/we_api/pay/api/micropay.py
Normal file
64
sg_wechat_enterprise/we_api/pay/api/micropay.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from wechatpy.pay.utils import get_external_ip
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
|
||||
|
||||
class WeChatMicroPay(BaseWeChatPayAPI):
|
||||
def create(self, body, total_fee, auth_code, client_ip=None, out_trade_no=None, detail=None, attach=None,
|
||||
fee_type='CNY', goods_tag=None, device_info=None, limit_pay=None):
|
||||
"""
|
||||
刷卡支付接口
|
||||
:param device_info: 可选,终端设备号(商户自定义,如门店编号)
|
||||
:param body: 商品描述
|
||||
:param detail: 可选,商品详情
|
||||
:param attach: 可选,附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
|
||||
:param client_ip: 可选,APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP
|
||||
:param out_trade_no: 可选,商户订单号,默认自动生成
|
||||
:param total_fee: 总金额,单位分
|
||||
:param fee_type: 可选,符合ISO 4217标准的三位字母代码,默认人民币:CNY
|
||||
:param goods_tag: 可选,商品标记,代金券或立减优惠功能的参数
|
||||
:param limit_pay: 可选,指定支付方式,no_credit--指定不能使用信用卡支付
|
||||
:param auth_code: 授权码,扫码支付授权码,设备读取用户微信中的条码或者二维码信息
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
now = datetime.now()
|
||||
if not out_trade_no:
|
||||
out_trade_no = '{0}{1}{2}'.format(
|
||||
self.mch_id,
|
||||
now.strftime('%Y%m%d%H%M%S'),
|
||||
random.randint(1000, 10000)
|
||||
)
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'device_info': device_info,
|
||||
'body': body,
|
||||
'detail': detail,
|
||||
'attach': attach,
|
||||
'out_trade_no': out_trade_no,
|
||||
'total_fee': total_fee,
|
||||
'fee_type': fee_type,
|
||||
'spbill_create_ip': client_ip or get_external_ip(),
|
||||
'goods_tag': goods_tag,
|
||||
'limit_pay': limit_pay,
|
||||
'auth_code': auth_code,
|
||||
}
|
||||
return self._post('pay/micropay', data=data)
|
||||
|
||||
def query(self, transaction_id=None, out_trade_no=None):
|
||||
"""
|
||||
查询订单
|
||||
|
||||
:param transaction_id: 微信的订单号,优先使用
|
||||
:param out_trade_no: 商户系统内部的订单号,当没提供transaction_id时需要传这个。
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'transaction_id': transaction_id,
|
||||
'out_trade_no': out_trade_no,
|
||||
}
|
||||
return self._post('pay/orderquery', data=data)
|
||||
138
sg_wechat_enterprise/we_api/pay/api/order.py
Normal file
138
sg_wechat_enterprise/we_api/pay/api/order.py
Normal file
@@ -0,0 +1,138 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from wechatpy.utils import timezone
|
||||
from wechatpy.pay.utils import get_external_ip
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
from wechatpy.utils import random_string, to_text
|
||||
from wechatpy.pay.utils import calculate_signature
|
||||
|
||||
|
||||
class WeChatOrder(BaseWeChatPayAPI):
|
||||
|
||||
def create(self, trade_type, body, total_fee, notify_url, client_ip=None,
|
||||
user_id=None, out_trade_no=None, detail=None, attach=None,
|
||||
fee_type='CNY', time_start=None, time_expire=None,
|
||||
goods_tag=None, product_id=None, device_info=None, limit_pay=None):
|
||||
"""
|
||||
统一下单接口
|
||||
|
||||
:param trade_type: 交易类型,取值如下:JSAPI,NATIVE,APP,WAP
|
||||
:param body: 商品描述
|
||||
:param total_fee: 总金额,单位分
|
||||
:param notify_url: 接收微信支付异步通知回调地址
|
||||
:param client_ip: 可选,APP和网页支付提交用户端ip,Native支付填调用微信支付API的机器IP
|
||||
:param user_id: 可选,用户在商户appid下的唯一标识。trade_type=JSAPI,此参数必传
|
||||
:param out_trade_no: 可选,商户订单号,默认自动生成
|
||||
:param detail: 可选,商品详情
|
||||
:param attach: 可选,附加数据,在查询API和支付通知中原样返回,该字段主要用于商户携带订单的自定义数据
|
||||
:param fee_type: 可选,符合ISO 4217标准的三位字母代码,默认人民币:CNY
|
||||
:param time_start: 可选,订单生成时间,默认为当前时间
|
||||
:param time_expire: 可选,订单失效时间,默认为订单生成时间后两小时
|
||||
:param goods_tag: 可选,商品标记,代金券或立减优惠功能的参数
|
||||
:param product_id: 可选,trade_type=NATIVE,此参数必传。此id为二维码中包含的商品ID,商户自行定义
|
||||
:param device_info: 可选,终端设备号(门店号或收银设备ID),注意:PC网页或公众号内支付请传"WEB"
|
||||
:param limit_pay: 可选,指定支付方式,no_credit--指定不能使用信用卡支付
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
now = datetime.fromtimestamp(time.time(), tz=timezone('Asia/Shanghai'))
|
||||
hours_later = now + timedelta(hours=2)
|
||||
if time_start is None:
|
||||
time_start = now
|
||||
if time_expire is None:
|
||||
time_expire = hours_later
|
||||
if not out_trade_no:
|
||||
out_trade_no = '{0}{1}{2}'.format(
|
||||
self.mch_id,
|
||||
now.strftime('%Y%m%d%H%M%S'),
|
||||
random.randint(1000, 10000)
|
||||
)
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'device_info': device_info,
|
||||
'body': body,
|
||||
'detail': detail,
|
||||
'attach': attach,
|
||||
'out_trade_no': out_trade_no,
|
||||
'fee_type': fee_type,
|
||||
'total_fee': total_fee,
|
||||
'spbill_create_ip': client_ip or get_external_ip(),
|
||||
'time_start': time_start.strftime('%Y%m%d%H%M%S'),
|
||||
'time_expire': time_expire.strftime('%Y%m%d%H%M%S'),
|
||||
'goods_tag': goods_tag,
|
||||
'notify_url': notify_url,
|
||||
'trade_type': trade_type,
|
||||
'limit_pay': limit_pay,
|
||||
'product_id': product_id,
|
||||
'openid': user_id,
|
||||
}
|
||||
return self._post('pay/unifiedorder', data=data)
|
||||
|
||||
def query(self, transaction_id=None, out_trade_no=None):
|
||||
"""
|
||||
查询订单
|
||||
|
||||
:param transaction_id: 微信的订单号,优先使用
|
||||
:param out_trade_no: 商户系统内部的订单号,当没提供transaction_id时需要传这个。
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'transaction_id': transaction_id,
|
||||
'out_trade_no': out_trade_no,
|
||||
}
|
||||
return self._post('pay/orderquery', data=data)
|
||||
|
||||
def close(self, out_trade_no):
|
||||
"""
|
||||
关闭订单
|
||||
|
||||
:param out_trade_no: 商户系统内部的订单号
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'out_trade_no': out_trade_no,
|
||||
}
|
||||
return self._post('pay/closeorder', data=data)
|
||||
|
||||
def get_appapi_params(self, prepay_id, timestamp=None, nonce_str=None):
|
||||
"""
|
||||
获取 APP 支付参数
|
||||
|
||||
:param prepay_id: 统一下单接口返回的 prepay_id 参数值
|
||||
:param timestamp: 可选,时间戳,默认为当前时间戳
|
||||
:param nonce_str: 可选,随机字符串,默认自动生成
|
||||
:return: 签名
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'partnerid': self.mch_id,
|
||||
'prepayid': prepay_id,
|
||||
'package': 'Sign=WXPay',
|
||||
'timestamp': timestamp or to_text(int(time.time())),
|
||||
'noncestr': nonce_str or random_string(32)
|
||||
}
|
||||
sign = calculate_signature(data, self._client.api_key)
|
||||
data['sign'] = sign
|
||||
return data
|
||||
|
||||
def reverse(self, transaction_id=None, out_trade_no=None):
|
||||
"""
|
||||
撤销订单
|
||||
|
||||
:param transaction_id: 可选,微信的订单号,优先使用
|
||||
:param out_trade_no: 可选,商户系统内部的订单号,
|
||||
transaction_id、out_trade_no二选一,
|
||||
如果同时存在优先级:transaction_id> out_trade_no
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'transaction_id': transaction_id,
|
||||
'out_trade_no': out_trade_no,
|
||||
}
|
||||
return self._post('secapi/pay/reverse', data=data)
|
||||
125
sg_wechat_enterprise/we_api/pay/api/redpack.py
Normal file
125
sg_wechat_enterprise/we_api/pay/api/redpack.py
Normal file
@@ -0,0 +1,125 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from wechatpy.pay.utils import get_external_ip
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
|
||||
|
||||
class WeChatRedpack(BaseWeChatPayAPI):
|
||||
|
||||
def send(self, user_id, total_amount, send_name, act_name,
|
||||
wishing, remark, total_num=1, client_ip=None,
|
||||
nick_name=None, min_value=None,
|
||||
max_value=None, out_trade_no=None, logo_imgurl=None):
|
||||
"""
|
||||
发送现金红包
|
||||
|
||||
:param user_id: 接收红包的用户在公众号下的 openid
|
||||
:param total_amount: 红包金额,单位分
|
||||
:param send_name: 商户名称
|
||||
:param nick_name: 可选,提供方名称,默认和商户名称相同
|
||||
:param act_name: 活动名称
|
||||
:param wishing: 红包祝福语
|
||||
:param remark: 备注
|
||||
:param client_ip: 可选,调用接口的机器 IP 地址
|
||||
:param total_num: 可选,红包发放总人数,默认为 1
|
||||
:param min_value: 可选,最小红包金额,单位分
|
||||
:param max_value: 可选,最大红包金额,单位分
|
||||
:param out_trade_no: 可选,商户订单号,默认会自动生成
|
||||
:param logo_imgurl: 可选,商户 Logo 的 URL
|
||||
:return: 返回的结果数据字典
|
||||
"""
|
||||
if not out_trade_no:
|
||||
now = datetime.now()
|
||||
out_trade_no = '{0}{1}{2}'.format(
|
||||
self.mch_id,
|
||||
now.strftime('%Y%m%d%H%M%S'),
|
||||
random.randint(1000, 10000)
|
||||
)
|
||||
data = {
|
||||
'wxappid': self.appid,
|
||||
're_openid': user_id,
|
||||
'total_amount': total_amount,
|
||||
'nick_name': nick_name or send_name,
|
||||
'send_name': send_name,
|
||||
'act_name': act_name,
|
||||
'wishing': wishing,
|
||||
'remark': remark,
|
||||
'client_ip': client_ip or get_external_ip(),
|
||||
'total_num': total_num,
|
||||
'min_value': min_value or total_amount,
|
||||
'max_value': max_value or total_amount,
|
||||
'mch_billno': out_trade_no,
|
||||
'logo_imgurl': logo_imgurl,
|
||||
}
|
||||
return self._post('mmpaymkttransfers/sendredpack', data=data)
|
||||
|
||||
def send_group(self, user_id, total_amount, send_name, act_name, wishing,
|
||||
remark, total_num, client_ip=None, amt_type="ALL_RAND",
|
||||
amt_list=None, out_trade_no=None,
|
||||
logo_imgurl=None, watermark_imgurl=None,
|
||||
banner_imgurl=None):
|
||||
"""
|
||||
发送裂变红包
|
||||
|
||||
:param user_id: 接收红包的用户在公众号下的 openid
|
||||
:param total_amount: 红包金额,单位分
|
||||
:param send_name: 商户名称
|
||||
:param act_name: 活动名称
|
||||
:param wishing: 红包祝福语
|
||||
:param remark: 备注
|
||||
:param total_num: 红包发放总人数
|
||||
:param client_ip: 可选,调用接口的机器 IP 地址
|
||||
:param amt_type: 可选,红包金额设置方式
|
||||
ALL_RAND—全部随机,商户指定总金额和红包发放总人数,由微信支付随机计算出各红包金额
|
||||
ALL_SPECIFIED—全部自定义
|
||||
SEED_SPECIFIED—种子红包自定义,其他随机
|
||||
:param amt_list: 可选,各红包具体金额,自定义金额时必须设置,单位分
|
||||
:param out_trade_no: 可选,商户订单号,默认会自动生成
|
||||
:param logo_imgurl: 可选,商户 Logo 的 URL
|
||||
:param watermark_imgurl: 可选,背景水印图片 URL
|
||||
:param banner_imgurl: 红包详情页面的 banner 图片 URL
|
||||
:return: 返回的结果数据字典
|
||||
"""
|
||||
if not out_trade_no:
|
||||
now = datetime.now()
|
||||
out_trade_no = '{0}{1}{2}'.format(
|
||||
self._client.mch_id,
|
||||
now.strftime('%Y%m%d%H%M%S'),
|
||||
random.randint(1000, 10000)
|
||||
)
|
||||
data = {
|
||||
'wxappid': self.appid,
|
||||
're_openid': user_id,
|
||||
'total_amount': total_amount,
|
||||
'send_name': send_name,
|
||||
'act_name': act_name,
|
||||
'wishing': wishing,
|
||||
'remark': remark,
|
||||
'total_num': total_num,
|
||||
'client_ip': client_ip or get_external_ip(),
|
||||
'amt_type': amt_type,
|
||||
'amt_list': amt_list,
|
||||
'mch_billno': out_trade_no,
|
||||
'logo_imgurl': logo_imgurl,
|
||||
'watermark_imgurl': watermark_imgurl,
|
||||
'banner_imgurl': banner_imgurl,
|
||||
}
|
||||
return self._post('mmpaymkttransfers/sendgroupredpack', data=data)
|
||||
|
||||
def query(self, out_trade_no, bill_type='MCHT'):
|
||||
"""
|
||||
查询红包发放记录
|
||||
|
||||
:param out_trade_no: 商户订单号
|
||||
:param bill_type: 可选,订单类型,目前固定为 MCHT
|
||||
:return: 返回的红包发放记录信息
|
||||
"""
|
||||
data = {
|
||||
'mch_billno': out_trade_no,
|
||||
'bill_type': bill_type,
|
||||
'appid': self.appid,
|
||||
}
|
||||
return self._post('mmpaymkttransfers/gethbinfo', data=data)
|
||||
61
sg_wechat_enterprise/we_api/pay/api/refund.py
Normal file
61
sg_wechat_enterprise/we_api/pay/api/refund.py
Normal file
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
|
||||
|
||||
class WeChatRefund(BaseWeChatPayAPI):
|
||||
|
||||
def apply(self, total_fee, refund_fee, out_refund_no, transaction_id=None,
|
||||
out_trade_no=None, fee_type='CNY', op_user_id=None,
|
||||
device_info=None,notify_url=None,refund_account='REFUND_SOURCE_UNSETTLED_FUNDS'):
|
||||
"""
|
||||
申请退款
|
||||
|
||||
:param total_fee: 订单总金额,单位为分
|
||||
:param refund_fee: 退款总金额,单位为分
|
||||
:param out_refund_no: 商户系统内部的退款单号,商户系统内部唯一,同一退款单号多次请求只退一笔
|
||||
:param transaction_id: 可选,微信订单号
|
||||
:param out_trade_no: 可选,商户系统内部的订单号,与 transaction_id 二选一
|
||||
:param fee_type: 可选,货币类型,符合ISO 4217标准的三位字母代码,默认人民币:CNY
|
||||
:param op_user_id: 可选,操作员帐号, 默认为商户号
|
||||
:param refund_account: 可选,退款资金来源,仅针对老资金流商户使用,默认使用未结算资金退款
|
||||
:param device_info: 可选,终端设备号
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'device_info': device_info,
|
||||
'transaction_id': transaction_id,
|
||||
'out_trade_no': out_trade_no,
|
||||
'out_refund_no': out_refund_no,
|
||||
'total_fee': total_fee,
|
||||
'refund_fee': refund_fee,
|
||||
'refund_fee_type': fee_type,
|
||||
'op_user_id': op_user_id if op_user_id else self.mch_id,
|
||||
"notify_url":notify_url,
|
||||
'refund_account': refund_account,
|
||||
|
||||
}
|
||||
return self._post('secapi/pay/refund', data=data)
|
||||
|
||||
def query(self, refund_id=None, out_refund_no=None, transaction_id=None,
|
||||
out_trade_no=None, device_info=None):
|
||||
"""
|
||||
查询退款
|
||||
|
||||
:param refund_id: 微信退款单号
|
||||
:param out_refund_no: 商户退款单号
|
||||
:param transaction_id: 微信订单号
|
||||
:param out_trade_no: 商户系统内部的订单号
|
||||
:param device_info: 可选,终端设备号
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'device_info': device_info,
|
||||
'transaction_id': transaction_id,
|
||||
'out_trade_no': out_trade_no,
|
||||
'out_refund_no': out_refund_no,
|
||||
'refund_id': refund_id,
|
||||
}
|
||||
return self._post('pay/refundquery', data=data)
|
||||
57
sg_wechat_enterprise/we_api/pay/api/tools.py
Normal file
57
sg_wechat_enterprise/we_api/pay/api/tools.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from datetime import datetime, date
|
||||
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
|
||||
|
||||
class WeChatTools(BaseWeChatPayAPI):
|
||||
|
||||
def short_url(self, long_url):
|
||||
"""
|
||||
长链接转短链接
|
||||
|
||||
:param long_url: 长链接
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'long_url': long_url,
|
||||
}
|
||||
return self._post('tools/shorturl', data=data)
|
||||
|
||||
def download_bill(self, bill_date, bill_type='ALL', device_info=None):
|
||||
"""
|
||||
下载对账单
|
||||
|
||||
:param bill_date: 下载对账单的日期
|
||||
:param bill_type: 账单类型,ALL,返回当日所有订单信息,默认值
|
||||
SUCCESS,返回当日成功支付的订单,
|
||||
REFUND,返回当日退款订单,
|
||||
REVOKED,已撤销的订单
|
||||
:param device_info: 微信支付分配的终端设备号,填写此字段,只下载该设备号的对账单
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
if isinstance(bill_date, (datetime, date)):
|
||||
bill_date = bill_date.strftime('%Y%m%d')
|
||||
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'bill_date': bill_date,
|
||||
'bill_type': bill_type,
|
||||
'device_info': device_info,
|
||||
}
|
||||
return self._post('pay/downloadbill', data=data)
|
||||
|
||||
def auto_code_to_openid(self, auth_code):
|
||||
"""
|
||||
授权码查询 openid 接口
|
||||
|
||||
:param auth_code: 扫码支付授权码,设备读取用户微信中的条码或者二维码信息
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'auth_code': auth_code,
|
||||
}
|
||||
return self._post('tools/authcodetoopenid', data=data)
|
||||
65
sg_wechat_enterprise/we_api/pay/api/transfer.py
Normal file
65
sg_wechat_enterprise/we_api/pay/api/transfer.py
Normal file
@@ -0,0 +1,65 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import random
|
||||
from datetime import datetime
|
||||
|
||||
from wechatpy.pay.utils import get_external_ip
|
||||
from wechatpy.pay.base import BaseWeChatPayAPI
|
||||
|
||||
|
||||
class WeChatTransfer(BaseWeChatPayAPI):
|
||||
|
||||
def transfer(self, user_id, amount, desc, client_ip=None,
|
||||
check_name='OPTION_CHECK', real_name=None,
|
||||
out_trade_no=None, device_info=None):
|
||||
"""
|
||||
企业付款接口
|
||||
|
||||
:param user_id: 接受收红包的用户在公众号下的 openid
|
||||
:param amount: 付款金额,单位分
|
||||
:param desc: 付款说明
|
||||
:param client_ip: 可选,调用接口机器的 IP 地址
|
||||
:param check_name: 可选,校验用户姓名选项,
|
||||
NO_CHECK:不校验真实姓名,
|
||||
FORCE_CHECK:强校验真实姓名(未实名认证的用户会校验失败,无法转账),
|
||||
OPTION_CHECK:针对已实名认证的用户才校验真实姓名(未实名认证用户不校验,可以转账成功),
|
||||
默认为 OPTION_CHECK
|
||||
:param real_name: 可选,收款用户真实姓名,
|
||||
如果check_name设置为FORCE_CHECK或OPTION_CHECK,则必填用户真实姓名
|
||||
:param out_trade_no: 可选,商户订单号,需保持唯一性,默认自动生成
|
||||
:param device_info: 可选,微信支付分配的终端设备号
|
||||
:return: 返回的结果信息
|
||||
"""
|
||||
if not out_trade_no:
|
||||
now = datetime.now()
|
||||
out_trade_no = '{0}{1}{2}'.format(
|
||||
self.mch_id,
|
||||
now.strftime('%Y%m%d%H%M%S'),
|
||||
random.randint(1000, 10000)
|
||||
)
|
||||
data = {
|
||||
'mch_appid': self.appid,
|
||||
'mchid': self.mch_id,
|
||||
'device_info': device_info,
|
||||
'partner_trade_no': out_trade_no,
|
||||
'openid': user_id,
|
||||
'check_name': check_name,
|
||||
're_user_name': real_name,
|
||||
'amount': amount,
|
||||
'desc': desc,
|
||||
'spbill_create_ip': client_ip or get_external_ip(),
|
||||
}
|
||||
return self._post('mmpaymkttransfers/promotion/transfers', data=data)
|
||||
|
||||
def query(self, out_trade_no):
|
||||
"""
|
||||
企业付款查询接口
|
||||
|
||||
:param out_trade_no: 商户调用企业付款API时使用的商户订单号
|
||||
:return: 返回的结果数据
|
||||
"""
|
||||
data = {
|
||||
'appid': self.appid,
|
||||
'partner_trade_no': out_trade_no,
|
||||
}
|
||||
return self._post('mmpaymkttransfers/gettransferinfo', data=data)
|
||||
30
sg_wechat_enterprise/we_api/pay/base.py
Normal file
30
sg_wechat_enterprise/we_api/pay/base.py
Normal file
@@ -0,0 +1,30 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
class BaseWeChatPayAPI(object):
|
||||
""" WeChat Pay 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 appid(self):
|
||||
return self._client.appid
|
||||
|
||||
@property
|
||||
def mch_id(self):
|
||||
return self._client.mch_id
|
||||
|
||||
@property
|
||||
def sub_mch_id(self):
|
||||
return self._client.sub_mch_id
|
||||
54
sg_wechat_enterprise/we_api/pay/utils.py
Normal file
54
sg_wechat_enterprise/we_api/pay/utils.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import copy
|
||||
import hashlib
|
||||
import socket
|
||||
|
||||
import six
|
||||
|
||||
from wechatpy.utils import to_binary, to_text
|
||||
|
||||
|
||||
def format_url(params, api_key=None):
|
||||
data = [to_binary('{0}={1}'.format(k, params[k])) for k in sorted(params) if params[k]]
|
||||
if api_key:
|
||||
data.append(to_binary('key={0}'.format(api_key)))
|
||||
return b"&".join(data)
|
||||
|
||||
|
||||
def calculate_signature(params, api_key):
|
||||
url = format_url(params, api_key)
|
||||
return to_text(hashlib.md5(url).hexdigest().upper())
|
||||
|
||||
|
||||
def _check_signature(params, api_key):
|
||||
_params = copy.deepcopy(params)
|
||||
sign = _params.pop('sign', '')
|
||||
return sign == calculate_signature(_params, api_key)
|
||||
|
||||
|
||||
def dict_to_xml(d, sign):
|
||||
xml = ['<xml>\n']
|
||||
for k in sorted(d):
|
||||
# use sorted to avoid test error on Py3k
|
||||
v = d[k]
|
||||
if isinstance(v, six.integer_types) or v.isdigit():
|
||||
xml.append('<{0}>{1}</{0}>\n'.format(to_text(k), to_text(v)))
|
||||
else:
|
||||
xml.append(
|
||||
'<{0}><![CDATA[{1}]]></{0}>\n'.format(to_text(k), to_text(v))
|
||||
)
|
||||
xml.append('<sign><![CDATA[{0}]]></sign>\n</xml>'.format(to_text(sign)))
|
||||
return ''.join(xml)
|
||||
|
||||
|
||||
def get_external_ip():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
try:
|
||||
wechat_ip = socket.gethostbyname('api.mch.weixin.qq.com')
|
||||
sock.connect((wechat_ip, 80))
|
||||
addr, port = sock.getsockname()
|
||||
sock.close()
|
||||
return addr
|
||||
except socket.error:
|
||||
return '127.0.0.1'
|
||||
341
sg_wechat_enterprise/we_api/replies.py
Normal file
341
sg_wechat_enterprise/we_api/replies.py
Normal file
@@ -0,0 +1,341 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.replies
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
This module defines all kinds of replies you can send to WeChat
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import time
|
||||
import six
|
||||
|
||||
from wechatpy.fields import(
|
||||
StringField,
|
||||
IntegerField,
|
||||
ImageField,
|
||||
VoiceField,
|
||||
VideoField,
|
||||
MusicField,
|
||||
ArticlesField,
|
||||
Base64EncodeField,
|
||||
HardwareField,
|
||||
)
|
||||
from wechatpy.messages import BaseMessage, MessageMetaClass
|
||||
from wechatpy.utils import to_text, to_binary
|
||||
|
||||
|
||||
REPLY_TYPES = {}
|
||||
|
||||
|
||||
def register_reply(reply_type):
|
||||
def register(cls):
|
||||
REPLY_TYPES[reply_type] = cls
|
||||
return cls
|
||||
return register
|
||||
|
||||
|
||||
class BaseReply(six.with_metaclass(MessageMetaClass)):
|
||||
"""Base class for all replies"""
|
||||
source = StringField('FromUserName')
|
||||
target = StringField('ToUserName')
|
||||
time = IntegerField('CreateTime', time.time())
|
||||
type = 'unknown'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self._data = {}
|
||||
message = kwargs.pop('message', None)
|
||||
if message and isinstance(message, BaseMessage):
|
||||
if 'source' not in kwargs:
|
||||
kwargs['source'] = message.target
|
||||
if 'target' not in kwargs:
|
||||
kwargs['target'] = message.source
|
||||
if hasattr(message, 'agent') and 'agent' not in kwargs:
|
||||
kwargs['agent'] = message.agent
|
||||
if 'time' not in kwargs:
|
||||
kwargs['time'] = time.time()
|
||||
for name, value in kwargs.items():
|
||||
field = self._fields.get(name)
|
||||
if field:
|
||||
self._data[field.name] = value
|
||||
else:
|
||||
setattr(self, name, value)
|
||||
|
||||
def render(self):
|
||||
"""Render reply from Python object to XML string"""
|
||||
tpl = '<xml>\n{data}\n</xml>'
|
||||
nodes = []
|
||||
msg_type = '<MsgType><![CDATA[{msg_type}]]></MsgType>'.format(
|
||||
msg_type=self.type
|
||||
)
|
||||
nodes.append(msg_type)
|
||||
for name, field in self._fields.items():
|
||||
value = getattr(self, name, field.default)
|
||||
node_xml = field.to_xml(value)
|
||||
nodes.append(node_xml)
|
||||
data = '\n'.join(nodes)
|
||||
return tpl.format(data=data)
|
||||
|
||||
def __str__(self):
|
||||
if six.PY2:
|
||||
return to_binary(self.render())
|
||||
else:
|
||||
return to_text(self.render())
|
||||
|
||||
|
||||
@register_reply('empty')
|
||||
class EmptyReply(BaseReply):
|
||||
"""
|
||||
回复空串
|
||||
|
||||
微信服务器不会对此作任何处理,并且不会发起重试
|
||||
"""
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def render(self):
|
||||
return ''
|
||||
|
||||
|
||||
@register_reply('text')
|
||||
class TextReply(BaseReply):
|
||||
"""
|
||||
文本回复
|
||||
详情请参阅 http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html
|
||||
"""
|
||||
type = 'text'
|
||||
content = StringField('Content')
|
||||
|
||||
|
||||
@register_reply('image')
|
||||
class ImageReply(BaseReply):
|
||||
"""
|
||||
图片回复
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html
|
||||
"""
|
||||
type = 'image'
|
||||
image = ImageField('Image')
|
||||
|
||||
@property
|
||||
def media_id(self):
|
||||
return self.image
|
||||
|
||||
@media_id.setter
|
||||
def media_id(self, value):
|
||||
self.image = value
|
||||
|
||||
|
||||
@register_reply('voice')
|
||||
class VoiceReply(BaseReply):
|
||||
"""
|
||||
语音回复
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html
|
||||
"""
|
||||
type = 'voice'
|
||||
voice = VoiceField('Voice')
|
||||
|
||||
@property
|
||||
def media_id(self):
|
||||
return self.voice
|
||||
|
||||
@media_id.setter
|
||||
def media_id(self, value):
|
||||
self.voice = value
|
||||
|
||||
|
||||
@register_reply('video')
|
||||
class VideoReply(BaseReply):
|
||||
"""
|
||||
视频回复
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html
|
||||
"""
|
||||
type = 'video'
|
||||
video = VideoField('Video', {})
|
||||
|
||||
@property
|
||||
def media_id(self):
|
||||
return self.video.get('media_id')
|
||||
|
||||
@media_id.setter
|
||||
def media_id(self, value):
|
||||
video = self.video
|
||||
video['media_id'] = value
|
||||
self.video = video
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.video.get('title')
|
||||
|
||||
@title.setter
|
||||
def title(self, value):
|
||||
video = self.video
|
||||
video['title'] = value
|
||||
self.video = video
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.video.get('description')
|
||||
|
||||
@description.setter
|
||||
def description(self, value):
|
||||
video = self.video
|
||||
video['description'] = value
|
||||
self.video = video
|
||||
|
||||
|
||||
@register_reply('music')
|
||||
class MusicReply(BaseReply):
|
||||
"""
|
||||
音乐回复
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html
|
||||
"""
|
||||
type = 'music'
|
||||
music = MusicField('Music', {})
|
||||
|
||||
@property
|
||||
def thumb_media_id(self):
|
||||
return self.music.get('thumb_media_id')
|
||||
|
||||
@thumb_media_id.setter
|
||||
def thumb_media_id(self, value):
|
||||
music = self.music
|
||||
music['thumb_media_id'] = value
|
||||
self.music = music
|
||||
|
||||
@property
|
||||
def title(self):
|
||||
return self.music.get('title')
|
||||
|
||||
@title.setter
|
||||
def title(self, value):
|
||||
music = self.music
|
||||
music['title'] = value
|
||||
self.music = music
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return self.music.get('description')
|
||||
|
||||
@description.setter
|
||||
def description(self, value):
|
||||
music = self.music
|
||||
music['description'] = value
|
||||
self.music = music
|
||||
|
||||
@property
|
||||
def music_url(self):
|
||||
return self.music.get('music_url')
|
||||
|
||||
@music_url.setter
|
||||
def music_url(self, value):
|
||||
music = self.music
|
||||
music['music_url'] = value
|
||||
self.music = music
|
||||
|
||||
@property
|
||||
def hq_music_url(self):
|
||||
return self.music.get('hq_music_url')
|
||||
|
||||
@hq_music_url.setter
|
||||
def hq_music_url(self, value):
|
||||
music = self.music
|
||||
music['hq_music_url'] = value
|
||||
self.music = music
|
||||
|
||||
|
||||
@register_reply('news')
|
||||
class ArticlesReply(BaseReply):
|
||||
"""
|
||||
图文回复
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/9/2c15b20a16019ae613d413e30cac8ea1.html
|
||||
"""
|
||||
type = 'news'
|
||||
articles = ArticlesField('Articles', [])
|
||||
|
||||
def add_article(self, article):
|
||||
if len(self.articles) == 10:
|
||||
raise AttributeError("Can't add more than 10 articles"
|
||||
" in an ArticlesReply")
|
||||
articles = self.articles
|
||||
articles.append(article)
|
||||
self.articles = articles
|
||||
|
||||
|
||||
@register_reply('transfer_customer_service')
|
||||
class TransferCustomerServiceReply(BaseReply):
|
||||
"""
|
||||
将消息转发到多客服
|
||||
详情请参阅
|
||||
http://mp.weixin.qq.com/wiki/5/ae230189c9bd07a6b221f48619aeef35.html
|
||||
"""
|
||||
type = 'transfer_customer_service'
|
||||
|
||||
|
||||
@register_reply('device_text')
|
||||
class DeviceTextReply(BaseReply):
|
||||
type = 'device_text'
|
||||
device_type = StringField('DeviceType')
|
||||
device_id = StringField('DeviceID')
|
||||
session_id = StringField('SessionID')
|
||||
content = Base64EncodeField('Content')
|
||||
|
||||
|
||||
@register_reply('device_event')
|
||||
class DeviceEventReply(BaseReply):
|
||||
type = 'device_event'
|
||||
event = StringField('Event')
|
||||
device_type = StringField('DeviceType')
|
||||
device_id = StringField('DeviceID')
|
||||
session_id = StringField('SessionID')
|
||||
content = Base64EncodeField('Content')
|
||||
|
||||
|
||||
@register_reply('device_status')
|
||||
class DeviceStatusReply(BaseReply):
|
||||
type = 'device_status'
|
||||
device_type = StringField('DeviceType')
|
||||
device_id = StringField('DeviceID')
|
||||
status = StringField('DeviceStatus')
|
||||
|
||||
|
||||
@register_reply('hardware')
|
||||
class HardwareReply(BaseReply):
|
||||
type = 'hardware'
|
||||
func_flag = IntegerField('FuncFlag', 0)
|
||||
hardware = HardwareField('HardWare')
|
||||
|
||||
|
||||
def create_reply(reply, message=None, render=False):
|
||||
"""
|
||||
Create a reply quickly
|
||||
"""
|
||||
r = None
|
||||
if not reply:
|
||||
r = EmptyReply()
|
||||
elif isinstance(reply, BaseReply):
|
||||
r = reply
|
||||
if message:
|
||||
r.source = message.target
|
||||
r.target = message.source
|
||||
elif isinstance(reply, six.string_types):
|
||||
r = TextReply(
|
||||
message=message,
|
||||
content=reply
|
||||
)
|
||||
elif isinstance(reply, (tuple, list)):
|
||||
if len(reply) > 10:
|
||||
raise AttributeError("Can't add more than 10 articles"
|
||||
" in an ArticlesReply")
|
||||
r = ArticlesReply(
|
||||
message=message,
|
||||
articles=reply
|
||||
)
|
||||
if r and render:
|
||||
return r.render()
|
||||
return r
|
||||
23
sg_wechat_enterprise/we_api/session/__init__.py
Normal file
23
sg_wechat_enterprise/we_api/session/__init__.py
Normal file
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
|
||||
|
||||
class SessionStorage(object):
|
||||
|
||||
def get(self, key, default=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def set(self, key, value, ttl=None):
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete(self, key):
|
||||
raise NotImplementedError()
|
||||
|
||||
def __getitem__(self, key):
|
||||
self.get(key)
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.set(key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.delete(key)
|
||||
35
sg_wechat_enterprise/we_api/session/memcachedstorage.py
Normal file
35
sg_wechat_enterprise/we_api/session/memcachedstorage.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.session import SessionStorage
|
||||
from wechatpy.utils import to_text
|
||||
from wechatpy.utils import json
|
||||
|
||||
|
||||
class MemcachedStorage(SessionStorage):
|
||||
|
||||
def __init__(self, mc, prefix='wechatpy'):
|
||||
for method_name in ('get', 'set', 'delete'):
|
||||
assert hasattr(mc, method_name)
|
||||
self.mc = mc
|
||||
self.prefix = prefix
|
||||
|
||||
def key_name(self, key):
|
||||
return '{0}:{1}'.format(self.prefix, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
key = self.key_name(key)
|
||||
value = self.mc.get(key)
|
||||
if value is None:
|
||||
return default
|
||||
return json.loads(to_text(value))
|
||||
|
||||
def set(self, key, value, ttl=None):
|
||||
if value is None:
|
||||
return
|
||||
key = self.key_name(key)
|
||||
value = json.dumps(value)
|
||||
self.mc.set(key, value)
|
||||
|
||||
def delete(self, key):
|
||||
key = self.key_name(key)
|
||||
self.mc.delete(key)
|
||||
20
sg_wechat_enterprise/we_api/session/memorystorage.py
Normal file
20
sg_wechat_enterprise/we_api/session/memorystorage.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.session import SessionStorage
|
||||
|
||||
|
||||
class MemoryStorage(SessionStorage):
|
||||
|
||||
def __init__(self):
|
||||
self._data = {}
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self._data.get(key, default)
|
||||
|
||||
def set(self, key, value, ttl=None):
|
||||
if value is None:
|
||||
return
|
||||
self._data[key] = value
|
||||
|
||||
def delete(self, key):
|
||||
self._data.pop(key, None)
|
||||
35
sg_wechat_enterprise/we_api/session/redisstorage.py
Normal file
35
sg_wechat_enterprise/we_api/session/redisstorage.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.session import SessionStorage
|
||||
from wechatpy.utils import to_text
|
||||
from wechatpy.utils import json
|
||||
|
||||
|
||||
class RedisStorage(SessionStorage):
|
||||
|
||||
def __init__(self, redis, prefix='wechatpy'):
|
||||
for method_name in ('get', 'set', 'delete'):
|
||||
assert hasattr(redis, method_name)
|
||||
self.redis = redis
|
||||
self.prefix = prefix
|
||||
|
||||
def key_name(self, key):
|
||||
return '{0}:{1}'.format(self.prefix, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
key = self.key_name(key)
|
||||
value = self.redis.get(key)
|
||||
if value is None:
|
||||
return default
|
||||
return json.loads(to_text(value))
|
||||
|
||||
def set(self, key, value, ttl=None):
|
||||
if value is None:
|
||||
return
|
||||
key = self.key_name(key)
|
||||
value = json.dumps(value)
|
||||
self.redis.set(key, value, ex=ttl)
|
||||
|
||||
def delete(self, key):
|
||||
key = self.key_name(key)
|
||||
self.redis.delete(key)
|
||||
34
sg_wechat_enterprise/we_api/session/shovestorage.py
Normal file
34
sg_wechat_enterprise/we_api/session/shovestorage.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
from wechatpy.session import SessionStorage
|
||||
|
||||
|
||||
class ShoveStorage(SessionStorage):
|
||||
|
||||
def __init__(self, shove, prefix='wechatpy'):
|
||||
self.shove = shove
|
||||
self.prefix = prefix
|
||||
|
||||
def key_name(self, key):
|
||||
return '{0}:{1}'.format(self.prefix, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
key = self.key_name(key)
|
||||
try:
|
||||
return self.shove[key]
|
||||
except KeyError:
|
||||
return default
|
||||
|
||||
def set(self, key, value, ttl=None):
|
||||
if value is None:
|
||||
return
|
||||
|
||||
key = self.key_name(key)
|
||||
self.shove[key] = value
|
||||
|
||||
def delete(self, key):
|
||||
key = self.key_name(key)
|
||||
try:
|
||||
del self.shove[key]
|
||||
except KeyError:
|
||||
pass
|
||||
149
sg_wechat_enterprise/we_api/utils.py
Normal file
149
sg_wechat_enterprise/we_api/utils.py
Normal file
@@ -0,0 +1,149 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
wechatpy.utils
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
This module provides some useful utilities.
|
||||
|
||||
:copyright: (c) 2014 by messense.
|
||||
:license: MIT, see LICENSE for more details.
|
||||
"""
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import six
|
||||
import six.moves.urllib.parse as urlparse
|
||||
import sys
|
||||
import string
|
||||
import random
|
||||
import hashlib
|
||||
|
||||
try:
|
||||
'''Use simplejson if we can, fallback to json otherwise.'''
|
||||
import simplejson as json
|
||||
except ImportError:
|
||||
import json # NOQA
|
||||
|
||||
|
||||
class ObjectDict(dict):
|
||||
"""Makes a dictionary behave like an object, with attribute-style access.
|
||||
"""
|
||||
|
||||
def __getattr__(self, key):
|
||||
if key in self:
|
||||
return self[key]
|
||||
return None
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self[key] = value
|
||||
|
||||
|
||||
class WeChatSigner(object):
|
||||
"""WeChat data signer"""
|
||||
|
||||
def __init__(self, delimiter=b''):
|
||||
self._data = []
|
||||
self._delimiter = to_binary(delimiter)
|
||||
|
||||
def add_data(self, *args):
|
||||
"""Add data to signer"""
|
||||
for data in args:
|
||||
self._data.append(to_binary(data))
|
||||
|
||||
@property
|
||||
def signature(self):
|
||||
"""Get data signature"""
|
||||
self._data.sort()
|
||||
str_to_sign = self._delimiter.join(self._data)
|
||||
return hashlib.sha1(str_to_sign).hexdigest()
|
||||
|
||||
|
||||
def check_signature(token, signature, timestamp, nonce):
|
||||
"""Check WeChat callback signature, raises InvalidSignatureException
|
||||
if check failed.
|
||||
|
||||
:param token: WeChat callback token
|
||||
:param signature: WeChat callback signature sent by WeChat server
|
||||
:param timestamp: WeChat callback timestamp sent by WeChat server
|
||||
:param nonce: WeChat callback nonce sent by WeChat sever
|
||||
"""
|
||||
signer = WeChatSigner()
|
||||
signer.add_data(token, timestamp, nonce)
|
||||
if signer.signature != signature:
|
||||
from wechatpy.exceptions import InvalidSignatureException
|
||||
|
||||
raise InvalidSignatureException()
|
||||
|
||||
|
||||
def to_text(value, encoding='utf-8'):
|
||||
"""Convert value to unicode, default encoding is utf-8
|
||||
|
||||
:param value: Value to be converted
|
||||
:param encoding: Desired encoding
|
||||
"""
|
||||
if not value:
|
||||
return ''
|
||||
if isinstance(value, six.text_type):
|
||||
return value
|
||||
if isinstance(value, six.binary_type):
|
||||
return value.decode(encoding)
|
||||
return six.text_type(value)
|
||||
|
||||
|
||||
def to_binary(value, encoding='utf-8'):
|
||||
"""Convert value to binary string, default encoding is utf-8
|
||||
|
||||
:param value: Value to be converted
|
||||
:param encoding: Desired encoding
|
||||
"""
|
||||
if not value:
|
||||
return b''
|
||||
if isinstance(value, six.binary_type):
|
||||
return value
|
||||
if isinstance(value, six.text_type):
|
||||
return value.encode(encoding)
|
||||
return six.binary_type(value)
|
||||
|
||||
|
||||
def timezone(zone):
|
||||
"""Try to get timezone using pytz or python-dateutil
|
||||
|
||||
:param zone: timezone str
|
||||
:return: timezone tzinfo or None
|
||||
"""
|
||||
try:
|
||||
import pytz
|
||||
return pytz.timezone(zone)
|
||||
except ImportError:
|
||||
pass
|
||||
try:
|
||||
from dateutil.tz import gettz
|
||||
return gettz(zone)
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def random_string(length=16):
|
||||
rule = string.ascii_letters + string.digits
|
||||
rand_list = random.sample(rule, length)
|
||||
return ''.join(rand_list)
|
||||
|
||||
|
||||
def get_querystring(uri):
|
||||
"""Get Qeruystring information from uri.
|
||||
|
||||
:param uri: uri
|
||||
:return: querystring info or {}
|
||||
"""
|
||||
parts = urlparse.urlsplit(uri)
|
||||
if sys.version_info[:2] == (2, 6):
|
||||
query = parts.path
|
||||
if query.startswith('?'):
|
||||
query = query[1:]
|
||||
else:
|
||||
query = parts.query
|
||||
return urlparse.parse_qs(query)
|
||||
|
||||
|
||||
def byte2int(c):
|
||||
if six.PY2:
|
||||
return ord(c)
|
||||
return c
|
||||
Reference in New Issue
Block a user