优化工单模块,增加企微模块

This commit is contained in:
mgw
2024-07-10 15:58:47 +08:00
parent e8512b23e4
commit 6b140fe6dd
134 changed files with 12830 additions and 2 deletions

View 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())

View 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

View 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 会缓存起来。
# 如果外部已经缓存,这里只需要传入 appidcomponent和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

View 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

View 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

View 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
)

View 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

View 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

View 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
}
)

View 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
}
}
)

View 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

View 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')

View 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

View 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}
)

View 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
}
)

View 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

View 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

View 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

View 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

View 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
}
)

View 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
}
)

View 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
}
)

View 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
)

View 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

View 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

View 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)

View 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})

View 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
)

View 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

View 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

View 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

View 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: 模板ID0-默认模板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

View 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)

View 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)

View 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
)

View 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

View 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()

View 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]

View 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)

View 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

View 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
}
)

View File

@@ -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

View 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']

View 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
}
)

View 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)

View 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']

View 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

View 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
}
)

View 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)

View 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)

View 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
}
)

View 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']

View 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,
}
)

View 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,
}
)

View File

@@ -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']

View 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']

View 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']

View 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
)

View 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']

View 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)

View 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')

View 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)

View 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

View 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]

View 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)

View 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')
)

View 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

View 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

View 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)

View 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

View 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

View 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)

View 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

View 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和网页支付提交用户端ipNative支付填调用微信支付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)

View 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: 交易类型取值如下JSAPINATIVEAPPWAP
:param body: 商品描述
:param total_fee: 总金额,单位分
:param notify_url: 接收微信支付异步通知回调地址
:param client_ip: 可选APP和网页支付提交用户端ipNative支付填调用微信支付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)

View 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)

View 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)

View 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)

View 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)

View 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

View 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'

View 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

View 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)

View 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)

View 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)

View 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)

View 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

View 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