parent
d6ce64c3b6
commit
9b8412304f
@ -0,0 +1,30 @@ |
||||
# Generated by Django 3.2.3 on 2021-09-07 19:18 |
||||
|
||||
from django.conf import settings |
||||
from django.db import migrations, models |
||||
import django.db.models.deletion |
||||
|
||||
|
||||
class Migration(migrations.Migration): |
||||
dependencies = [ |
||||
('api', '0004_auto_20210705_1819'), |
||||
] |
||||
|
||||
operations = [ |
||||
migrations.CreateModel( |
||||
name='ThirdWeChatUserInfo', |
||||
fields=[ |
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), |
||||
('openid', models.CharField(max_length=64, unique=True, verbose_name='普通用户的标识,对当前公众号唯一')), |
||||
('nickname', models.CharField(blank=True, max_length=64, verbose_name='昵称')), |
||||
('sex', models.SmallIntegerField(default=0, help_text='值为1时是男性,值为2时是女性,值为0时是未知', verbose_name='性别')), |
||||
('subscribe_time', models.BigIntegerField(verbose_name='订阅时间')), |
||||
('head_img_url', models.CharField(blank=True, max_length=256, null=True, verbose_name='用户头像')), |
||||
('address', models.CharField(blank=True, max_length=128, null=True, verbose_name='地址')), |
||||
('subscribe', models.BooleanField(default=0, verbose_name='是否订阅公众号')), |
||||
('created_time', models.DateTimeField(auto_now_add=True, verbose_name='授权时间')), |
||||
('user_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, |
||||
verbose_name='用户ID')), |
||||
], |
||||
), |
||||
] |
@ -0,0 +1,5 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 9月 |
||||
# author: NinEveN |
||||
# date: 2021/9/6 |
@ -0,0 +1,5 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 9月 |
||||
# author: NinEveN |
||||
# date: 2021/9/6 |
@ -0,0 +1,86 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 9月 |
||||
# author: NinEveN |
||||
# date: 2021/9/6 |
||||
|
||||
import xml.etree.ElementTree as ET |
||||
|
||||
|
||||
def parse_xml(web_data): |
||||
if len(web_data) == 0: |
||||
return None |
||||
xml_data = ET.fromstring(web_data) |
||||
msg_type = xml_data.find('MsgType').text |
||||
|
||||
if msg_type == 'event': |
||||
event_type = xml_data.find('Event').text |
||||
if event_type == 'CLICK': |
||||
return Click(xml_data) |
||||
elif event_type in ('subscribe', 'unsubscribe'): |
||||
return Subscribe(xml_data) |
||||
# elif event_type == 'VIEW': |
||||
# return View(xml_data) |
||||
# elif event_type == 'LOCATION': |
||||
# return LocationEvent(xml_data) |
||||
elif event_type == 'SCAN': |
||||
return Scan(xml_data) |
||||
|
||||
elif msg_type == 'text': |
||||
return TextMsg(xml_data) |
||||
elif msg_type == 'image': |
||||
return ImageMsg(xml_data) |
||||
|
||||
|
||||
class Msg(object): |
||||
def __init__(self, xml_data): |
||||
self.ToUserName = xml_data.find('ToUserName').text |
||||
self.FromUserName = xml_data.find('FromUserName').text |
||||
self.CreateTime = xml_data.find('CreateTime').text |
||||
self.MsgType = xml_data.find('MsgType').text |
||||
self.MsgId = xml_data.find('MsgId').text |
||||
|
||||
|
||||
class TextMsg(Msg): |
||||
def __init__(self, xml_data): |
||||
Msg.__init__(self, xml_data) |
||||
self.Content = xml_data.find('Content').text.encode("utf-8") |
||||
|
||||
|
||||
class ImageMsg(Msg): |
||||
def __init__(self, xml_data): |
||||
Msg.__init__(self, xml_data) |
||||
self.PicUrl = xml_data.find('PicUrl').text |
||||
self.MediaId = xml_data.find('MediaId').text |
||||
|
||||
|
||||
class EventMsg(object): |
||||
def __init__(self, xml_data): |
||||
self.ToUserName = xml_data.find('ToUserName').text |
||||
self.FromUserName = xml_data.find('FromUserName').text |
||||
self.CreateTime = xml_data.find('CreateTime').text |
||||
self.MsgType = xml_data.find('MsgType').text |
||||
self.Event = xml_data.find('Event').text |
||||
self.Eventkey = xml_data.find('EventKey').text |
||||
self.Ticket = '' |
||||
|
||||
|
||||
class Click(EventMsg): |
||||
def __init__(self, xml_data): |
||||
EventMsg.__init__(self, xml_data) |
||||
|
||||
|
||||
class Scan(EventMsg): |
||||
def __init__(self, xml_data): |
||||
EventMsg.__init__(self, xml_data) |
||||
self.Ticket = xml_data.find('Ticket').text |
||||
|
||||
|
||||
class Subscribe(EventMsg): |
||||
def __init__(self, xml_data): |
||||
EventMsg.__init__(self, xml_data) |
||||
tick = xml_data.find('Ticket') |
||||
if tick is None: |
||||
self.Ticket = '' |
||||
else: |
||||
self.Ticket = tick.text |
@ -0,0 +1,61 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 9月 |
||||
# author: NinEveN |
||||
# date: 2021/9/6 |
||||
|
||||
import time |
||||
|
||||
|
||||
class Msg(object): |
||||
def __init__(self): |
||||
pass |
||||
|
||||
def send(self): |
||||
return "success" |
||||
|
||||
|
||||
class TextMsg(Msg): |
||||
def __init__(self, to_user_name, from_user_name, content): |
||||
super().__init__() |
||||
self.__dict = dict() |
||||
self.__dict['ToUserName'] = to_user_name |
||||
self.__dict['FromUserName'] = from_user_name |
||||
self.__dict['CreateTime'] = int(time.time()) |
||||
self.__dict['Content'] = content |
||||
|
||||
def send(self): |
||||
xml_form = """ |
||||
<xml> |
||||
<ToUserName><![CDATA[{ToUserName}]]></ToUserName> |
||||
<FromUserName><![CDATA[{FromUserName}]]></FromUserName> |
||||
<CreateTime>{CreateTime}</CreateTime> |
||||
<MsgType><![CDATA[text]]></MsgType> |
||||
<Content><![CDATA[{Content}]]></Content> |
||||
</xml> |
||||
""" |
||||
return xml_form.format(**self.__dict) |
||||
|
||||
|
||||
class ImageMsg(Msg): |
||||
def __init__(self, to_user_name, from_user_name, media_id): |
||||
super().__init__() |
||||
self.__dict = dict() |
||||
self.__dict['ToUserName'] = to_user_name |
||||
self.__dict['FromUserName'] = from_user_name |
||||
self.__dict['CreateTime'] = int(time.time()) |
||||
self.__dict['MediaId'] = media_id |
||||
|
||||
def send(self): |
||||
xml_form = """ |
||||
<xml> |
||||
<ToUserName><![CDATA[{ToUserName}]]></ToUserName> |
||||
<FromUserName><![CDATA[{FromUserName}]]></FromUserName> |
||||
<CreateTime>{CreateTime}</CreateTime> |
||||
<MsgType><![CDATA[image]]></MsgType> |
||||
<Image> |
||||
<MediaId><![CDATA[{MediaId}]]></MediaId> |
||||
</Image> |
||||
</xml> |
||||
""" |
||||
return xml_form.format(**self.__dict) |
@ -0,0 +1,20 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding: utf-8 -*- |
||||
######################################################################### |
||||
# Author: jonyqin |
||||
# Created Time: Thu 11 Sep 2014 01:53:58 PM CST |
||||
# File Name: ierror.py |
||||
# Description:定义错误码含义 |
||||
######################################################################### |
||||
WXBizMsgCrypt_OK = 0 |
||||
WXBizMsgCrypt_ValidateSignature_Error = -40001 |
||||
WXBizMsgCrypt_ParseXml_Error = -40002 |
||||
WXBizMsgCrypt_ComputeSignature_Error = -40003 |
||||
WXBizMsgCrypt_IllegalAesKey = -40004 |
||||
WXBizMsgCrypt_ValidateAppid_Error = -40005 |
||||
WXBizMsgCrypt_EncryptAES_Error = -40006 |
||||
WXBizMsgCrypt_DecryptAES_Error = -40007 |
||||
WXBizMsgCrypt_IllegalBuffer = -40008 |
||||
WXBizMsgCrypt_EncodeBase64_Error = -40009 |
||||
WXBizMsgCrypt_DecodeBase64_Error = -40010 |
||||
WXBizMsgCrypt_GenReturnXml_Error = -40011 |
@ -0,0 +1,220 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 9月 |
||||
# author: NinEveN |
||||
# date: 2021/9/6 |
||||
|
||||
import base64 |
||||
import logging |
||||
import string |
||||
import random |
||||
import struct |
||||
import hashlib |
||||
from Crypto.Cipher import AES |
||||
import xml.etree.cElementTree as ET |
||||
import time |
||||
import socket |
||||
from . import ierror |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
|
||||
class XMLParse: |
||||
"""提供提取消息格式中的密文及生成回复消息格式的接口""" |
||||
|
||||
# xml消息模板 |
||||
AES_TEXT_RESPONSE_TEMPLATE = """<xml> |
||||
<Encrypt><![CDATA[%(msg_encrypt)s]]></Encrypt> |
||||
<MsgSignature><![CDATA[%(msg_signature)s]]></MsgSignature> |
||||
<TimeStamp>%(timestamp)s</TimeStamp> |
||||
<Nonce><![CDATA[%(nonce)s]]></Nonce> |
||||
</xml>""" |
||||
|
||||
def extract(self, xml_text): |
||||
"""提取出xml数据包中的加密消息 |
||||
@param xml_text: 待提取的xml字符串 |
||||
@return: 提取出的加密消息字符串 |
||||
""" |
||||
try: |
||||
xml_tree = ET.fromstring(xml_text) |
||||
encrypt = xml_tree.find("Encrypt") |
||||
to_user_name = xml_tree.find("ToUserName") |
||||
return ierror.WXBizMsgCrypt_OK, encrypt.text, to_user_name.text |
||||
except Exception as e: |
||||
logger.error(e) |
||||
return ierror.WXBizMsgCrypt_ParseXml_Error, None, None |
||||
|
||||
def generate(self, encrypt, signature, timestamp, nonce): |
||||
"""生成xml消息 |
||||
@param encrypt: 加密后的消息密文 |
||||
@param signature: 安全签名 |
||||
@param timestamp: 时间戳 |
||||
@param nonce: 随机字符串 |
||||
@return: 生成的xml字符串 |
||||
""" |
||||
resp_dict = { |
||||
'msg_encrypt': encrypt, |
||||
'msg_signature': signature, |
||||
'timestamp': timestamp, |
||||
'nonce': nonce, |
||||
} |
||||
resp_xml = self.AES_TEXT_RESPONSE_TEMPLATE % resp_dict |
||||
return resp_xml |
||||
|
||||
|
||||
class PKCS7Encoder(object): |
||||
"""提供基于PKCS7算法的加解密接口""" |
||||
|
||||
block_size = 32 |
||||
|
||||
def encode(self, text): |
||||
""" 对需要加密的明文进行填充补位 |
||||
@param text: 需要进行填充补位操作的明文 |
||||
@return: 补齐明文字符串 |
||||
""" |
||||
text_length = len(text) |
||||
# 计算需要填充的位数 |
||||
amount_to_pad = self.block_size - (text_length % self.block_size) |
||||
if amount_to_pad == 0: |
||||
amount_to_pad = self.block_size |
||||
# 获得补位所用的字符 |
||||
pad = chr(amount_to_pad) |
||||
return text + pad * amount_to_pad |
||||
|
||||
def decode(self, decrypted): |
||||
"""删除解密后明文的补位字符 |
||||
@param decrypted: 解密后的明文 |
||||
@return: 删除补位字符后的明文 |
||||
""" |
||||
pad = ord(decrypted[-1]) |
||||
if pad < 1 or pad > 32: |
||||
pad = 0 |
||||
return decrypted[:-pad] |
||||
|
||||
|
||||
def get_random_str(): |
||||
""" 随机生成16位字符串 |
||||
@return: 16位字符串 |
||||
""" |
||||
rule = string.ascii_letters + string.digits |
||||
return "".join(random.sample(rule, 16)) |
||||
|
||||
|
||||
class Prpcrypt(object): |
||||
"""提供接收和推送给公众平台消息的加解密接口""" |
||||
|
||||
def __init__(self, key): |
||||
# self.key = base64.b64decode(key+"=") |
||||
self.key = key |
||||
# 设置加解密模式为AES的CBC模式 |
||||
self.mode = AES.MODE_CBC |
||||
|
||||
def encrypt(self, text, app_id): |
||||
"""对明文进行加密 |
||||
:param text: 需要加密的明文 |
||||
:param app_id: 应用id |
||||
@return: 加密得到的字符串 |
||||
""" |
||||
# 16位随机字符串添加到明文开头 |
||||
text = get_random_str() + struct.pack("I", socket.htonl(len(text))).decode('utf-8') + text + app_id |
||||
# 使用自定义的填充方式对明文进行补位填充 |
||||
pkcs7 = PKCS7Encoder() |
||||
text = pkcs7.encode(text) |
||||
# 加密 |
||||
crypto = AES.new(self.key, self.mode, self.key[:16]) |
||||
try: |
||||
ciphertext = crypto.encrypt(text) |
||||
# 使用BASE64对加密后的字符串进行编码 |
||||
return ierror.WXBizMsgCrypt_OK, base64.b64encode(ciphertext) |
||||
except Exception as e: |
||||
logger.error(e) |
||||
return ierror.WXBizMsgCrypt_EncryptAES_Error, None |
||||
|
||||
def decrypt(self, text, app_id): |
||||
""" 对解密后的明文进行补位删除 |
||||
:param text: 密文 |
||||
:param app_id: 应用id |
||||
:return: 删除填充补位后的明文 |
||||
""" |
||||
try: |
||||
cryptor = AES.new(self.key, self.mode, self.key[:16]) |
||||
# 使用BASE64对密文进行解码,然后AES-CBC解密 |
||||
plain_text = cryptor.decrypt(base64.b64decode(text)) |
||||
except Exception as e: |
||||
logger.error(e) |
||||
return ierror.WXBizMsgCrypt_DecryptAES_Error, None |
||||
try: |
||||
pad = plain_text[-1] |
||||
# 去掉补位字符串 |
||||
# pkcs7 = PKCS7Encoder() |
||||
# plain_text = pkcs7.encode(plain_text) |
||||
# 去除16位随机字符串 |
||||
content = plain_text[16:-pad] |
||||
xml_len = socket.ntohl(struct.unpack("I", content[: 4])[0]) |
||||
xml_content = content[4: xml_len + 4] |
||||
from_app_id = content[xml_len + 4:].decode('utf-8') |
||||
except Exception as e: |
||||
logger.error(e) |
||||
return ierror.WXBizMsgCrypt_IllegalBuffer, None |
||||
if from_app_id != app_id: |
||||
return ierror.WXBizMsgCrypt_ValidateAppid_Error, None |
||||
return 0, xml_content |
||||
|
||||
|
||||
class WxMsgCryptBase(object): |
||||
def __init__(self, app_id, app_secret, token, encoding_aes_key): |
||||
try: |
||||
self.key = base64.b64decode(encoding_aes_key + "=") |
||||
assert len(self.key) == 32 |
||||
except Exception as e: |
||||
logger.error(f"{encoding_aes_key} 解密失败 Exception:{e}") |
||||
raise Exception("[error]: EncodingAESKey invalid !") |
||||
self.token = token |
||||
self.app_id = app_id |
||||
|
||||
def encrypt_msg(self, msg, nonce, timestamp=None): |
||||
""" |
||||
:param msg: 企业号待回复用户的消息,xml格式的字符串 |
||||
:param nonce: 随机串,可以自己生成,也可以用URL参数的nonce |
||||
:param timestamp: 时间戳,可以自己生成,也可以用URL参数的timestamp,如为None则自动用当前时间 |
||||
:return: 成功0,sEncryptMsg,失败返回对应的错误码None |
||||
""" |
||||
pc = Prpcrypt(self.key) |
||||
ret, encrypt = pc.encrypt(msg, self.app_id) |
||||
if ret != 0: |
||||
return ret, None |
||||
if timestamp is None: |
||||
timestamp = str(int(time.time())) |
||||
|
||||
try: |
||||
sha = hashlib.sha1(("".join(sorted([self.token, timestamp, nonce, encrypt]))).encode('utf-8')) |
||||
return ret, XMLParse().generate(encrypt, sha.hexdigest(), timestamp, nonce) |
||||
except Exception as e: |
||||
logger.error(e) |
||||
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None |
||||
|
||||
def decrypt_msg(self, msg, msg_signature, timestamp, nonce): |
||||
""" |
||||
:param msg: 密文,对应POST请求的数据 |
||||
:param msg_signature: 签名串,对应URL参数的msg_signature |
||||
:param timestamp: 时间戳,对应URL参数的timestamp |
||||
:param nonce: 随机串,对应URL参数的nonce |
||||
:return: 成功0,失败返回对应的错误码 |
||||
""" |
||||
|
||||
if isinstance(msg, str): |
||||
ret, encrypt, _ = XMLParse().extract(msg) |
||||
if ret != 0: |
||||
return ret, None |
||||
else: |
||||
encrypt = msg.get('Encrypt') |
||||
try: |
||||
sha = hashlib.sha1(("".join(sorted([self.token, timestamp, nonce, encrypt]))).encode('utf-8')) |
||||
if not sha.hexdigest() == msg_signature: |
||||
return ierror.WXBizMsgCrypt_ValidateSignature_Error, None |
||||
pc = Prpcrypt(self.key) |
||||
ret, xml_content = pc.decrypt(encrypt, self.app_id) |
||||
return ret, xml_content |
||||
except Exception as e: |
||||
logger.error(e) |
||||
return ierror.WXBizMsgCrypt_ComputeSignature_Error, None |
@ -0,0 +1,119 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 9月 |
||||
# author: NinEveN |
||||
# date: 2021/9/6 |
||||
from hashlib import sha1 |
||||
import requests |
||||
import logging |
||||
import json |
||||
from fir_ser.settings import THIRDLOGINCONF |
||||
from api.utils.mp.utils import WxMsgCryptBase |
||||
from api.utils.storage.caches import get_wx_access_token_cache |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
wx_login_info = THIRDLOGINCONF.wx_official |
||||
|
||||
|
||||
def create_menu(): |
||||
menu_json = { |
||||
"button": [ |
||||
{ |
||||
"type": "click", |
||||
"name": "赞", |
||||
"key": "good" |
||||
}, |
||||
{ |
||||
"name": "分发平台", |
||||
"sub_button": [ |
||||
{ |
||||
"type": "view", |
||||
"name": "官方地址", |
||||
"url": "https://flyapps.cn" |
||||
}, |
||||
{ |
||||
"type": "view", |
||||
"name": "留言反馈", |
||||
"url": "https://flyapps.cn/gbook/" |
||||
}, |
||||
] |
||||
}, |
||||
{ |
||||
"type": "media_id", |
||||
"name": "联系我们", |
||||
"media_id": "qvQxPuAb4GnUgjkxl2xVnbsnldxawf4DXM09biqgP30" |
||||
} |
||||
] |
||||
} |
||||
p_url = f"https://api.weixin.qq.com/cgi-bin/menu/create?access_token={get_wx_access_token_cache()}" |
||||
req = requests.post(url=p_url, data=json.dumps(menu_json, ensure_ascii=False).encode('utf-8')) |
||||
print(req.json()) |
||||
|
||||
|
||||
def show_qrcode_url(ticket): |
||||
return f'https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={ticket}' |
||||
|
||||
|
||||
def make_wx_login_qrcode(scene_str='web.login', expire_seconds=600): |
||||
""" |
||||
:param scene_str: 场景值ID(字符串形式的ID),字符串类型,长度限制为1到64 |
||||
:param expire_seconds: 该二维码有效时间,以秒为单位。 最大不超过2592000(即30天),此字段如果不填,则默认有效期为30秒。 |
||||
:return: { |
||||
"ticket":"gQH47joAAAAAAAAAASxodHRwOi8vd2VpeGluLnFxLmNvbS9xL2taZ2Z3TVRtNzJXV1Brb3ZhYmJJAAIEZ23sUwMEmm3sUw==", |
||||
"expire_seconds":60, |
||||
"url":"http://weixin.qq.com/q/kZgfwMTm72WWPkovabbI" |
||||
} |
||||
https://developers.weixin.qq.com/doc/offiaccount/Account_Management/Generating_a_Parametric_QR_Code.html |
||||
""" |
||||
t_url = f'https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={get_wx_access_token_cache()}' |
||||
data = {"expire_seconds": expire_seconds, "action_name": "QR_STR_SCENE", |
||||
"action_info": {"scene": {"scene_str": scene_str}}} |
||||
req = requests.post(t_url, json=data) |
||||
if req.status_code == 200: |
||||
return True, req.json() |
||||
logger.error(f"make wx login qrcode failed {req.status_code} {req.text}") |
||||
return False, req.text |
||||
|
||||
|
||||
def get_userinfo_from_openid(open_id): |
||||
t_url = f'https://api.weixin.qq.com/cgi-bin/user/info?access_token={get_wx_access_token_cache()}&openid={open_id}&lang=zh_CN' |
||||
req = requests.get(t_url) |
||||
if req.status_code == 200: |
||||
return True, req.json() |
||||
logger.error(f"get userinfo from openid failed {req.status_code} {req.text}") |
||||
return False, req.text |
||||
|
||||
|
||||
class WxOfficialBase(object): |
||||
|
||||
def __init__(self, app_id, app_secret, token, encoding_aes_key): |
||||
self.app_id = app_id |
||||
self.app_secret = app_secret |
||||
self.token = token |
||||
self.encoding_aes_key = encoding_aes_key |
||||
|
||||
def get_access_token(self): |
||||
t_url = f'https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={self.app_id}&secret={self.app_secret}' |
||||
req = requests.get(t_url) |
||||
if req.status_code == 200: |
||||
return req.json() |
||||
logger.error(f"get access token failed {req.status_code} {req.text}") |
||||
return req.text |
||||
|
||||
|
||||
def make_wx_auth_obj(): |
||||
return WxOfficialBase(**wx_login_info.get('auth')) |
||||
|
||||
|
||||
def check_signature(params): |
||||
tmp_list = sorted([wx_login_info.get('auth', {}).get('token'), params.get("timestamp"), params.get("nonce")]) |
||||
tmp_str = "".join(tmp_list) |
||||
tmp_str = sha1(tmp_str.encode("utf-8")).hexdigest() |
||||
if tmp_str == params.get("signature"): |
||||
return int(params.get("echostr")) |
||||
return '' |
||||
|
||||
|
||||
class WxMsgCrypt(WxMsgCryptBase): |
||||
def __init__(self): |
||||
super().__init__(**wx_login_info.get('auth')) |
@ -0,0 +1,176 @@ |
||||
#!/usr/bin/env python |
||||
# -*- coding:utf-8 -*- |
||||
# project: 3月 |
||||
# author: NinEveN |
||||
# date: 2021/3/29 |
||||
from rest_framework.pagination import PageNumberPagination |
||||
from rest_framework.response import Response |
||||
import logging |
||||
import random |
||||
from rest_framework.views import APIView |
||||
from rest_framework_xml.parsers import XMLParser |
||||
from api.models import ThirdWeChatUserInfo, UserInfo |
||||
from api.utils.auth import ExpiringTokenAuthentication |
||||
|
||||
from api.utils.mp.chat import reply, receive |
||||
from api.utils.mp.wechat import check_signature, WxMsgCrypt, get_userinfo_from_openid |
||||
from api.utils.response import BaseResponse |
||||
from api.utils.serializer import ThirdWxSerializer |
||||
from api.utils.storage.caches import set_wx_ticket_login_info_cache |
||||
|
||||
logger = logging.getLogger(__name__) |
||||
|
||||
GOOD_XX = [ |
||||
'您是一位有恒心有毅力的人,我很佩服您!', |
||||
'越有内涵的人越虚怀若谷,像您这样有内涵的人我十分敬佩!', |
||||
'你像天上的月亮,也像那闪烁的星星,可惜我不是诗人,否则,当写一万首诗来形容你的美丽!', |
||||
'据考证,你是世界上最大的宝藏,里面藏满了金子、钻石和名画!', |
||||
'虽然你没有一簇樱唇两排贝齿,但你的谈吐高雅脱俗,机智过人,令我折服!', |
||||
'您是一位有恒心有毅力的人,我很佩服您!', |
||||
'我很荣幸,认识你这样有内涵的漂亮朋友!', |
||||
'春花秋月,是诗人们歌颂的情景,可是我对于它,却感到十分平凡。只有你嵌着梨涡的笑容,才是我眼中最美的景象!', |
||||
'你像一片轻柔的云在我眼前飘来飘去,你清丽秀雅的脸上荡漾着春天般美丽的笑容。在你那双又大又亮的眼睛里,我总能捕捉到你的宁静,你的热烈,你的聪颖,你的敏感!', |
||||
'人生旅程上,您丰富我的心灵,开发我得智力,为我点燃了希望的光芒,谢谢您!', |
||||
] |
||||
|
||||
|
||||
class TextXMLParser(XMLParser): |
||||
media_type = 'text/xml' # 微信解析的是 text/xml |
||||
|
||||
|
||||
def reply_login_msg(rec_msg, to_user, from_user, ): |
||||
content = f'还未绑定用户,请通过手机或者邮箱登录账户之后进行绑定' |
||||
u_data_id = -1 |
||||
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first() |
||||
if wx_user_obj: |
||||
u_data_id = wx_user_obj.user_id.pk |
||||
content = f'用户 {wx_user_obj.user_id.first_name} 登录成功' |
||||
set_wx_ticket_login_info_cache(rec_msg.Ticket, u_data_id) |
||||
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||
return reply_msg.send() |
||||
|
||||
|
||||
def update_or_create_wx_userinfo(to_user, user_obj): |
||||
code, wx_user_info = get_userinfo_from_openid(to_user) |
||||
logger.info(f"get openid:{to_user} info:{to_user} code:{code}") |
||||
if code: |
||||
wx_user_info = { |
||||
'openid': wx_user_info.get('openid'), |
||||
'nickname': wx_user_info.get('nickname'), |
||||
'sex': wx_user_info.get('sex'), |
||||
'subscribe_time': wx_user_info.get('subscribe_time'), |
||||
'head_img_url': wx_user_info.get('headimgurl'), |
||||
'address': f"{wx_user_info.get('country')}-{wx_user_info.get('province')}-{wx_user_info.get('city')}", |
||||
'subscribe': wx_user_info.get('subscribe'), |
||||
} |
||||
ThirdWeChatUserInfo.objects.update_or_create(user_id=user_obj, openid=to_user, defaults=wx_user_info) |
||||
|
||||
|
||||
class ValidWxChatToken(APIView): |
||||
parser_classes = (XMLParser, TextXMLParser) |
||||
|
||||
def get(self, request): |
||||
params = request.query_params |
||||
return Response(check_signature(params)) |
||||
|
||||
def post(self, request): |
||||
params = request.query_params |
||||
data = request.data |
||||
encrypt_obj = WxMsgCrypt() |
||||
ret, encrypt_xml = encrypt_obj.decrypt_msg(data, params.get("msg_signature"), params.get("timestamp"), |
||||
params.get("nonce")) |
||||
logger.info(f"code:{ret}, result {encrypt_xml}") |
||||
result = "success" |
||||
if ret == 0: |
||||
content = '欢迎使用fly应用分发平台,感谢您的关注' |
||||
rec_msg = receive.parse_xml(encrypt_xml) |
||||
if isinstance(rec_msg, receive.Msg): |
||||
to_user = rec_msg.FromUserName |
||||
from_user = rec_msg.ToUserName |
||||
|
||||
if rec_msg.MsgType == 'text': |
||||
content = random.choices([*GOOD_XX, content, rec_msg.Content.decode('utf-8')])[0] |
||||
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||
result = reply_msg.send() |
||||
|
||||
elif rec_msg.MsgType == 'image': |
||||
media_id = rec_msg.MediaId |
||||
reply_msg = reply.ImageMsg(to_user, from_user, media_id) |
||||
result = reply_msg.send() |
||||
else: |
||||
result = reply.Msg().send() |
||||
|
||||
elif isinstance(rec_msg, receive.EventMsg): |
||||
to_user = rec_msg.FromUserName |
||||
from_user = rec_msg.ToUserName |
||||
if rec_msg.Event == 'CLICK': # 公众号点击事件 |
||||
if rec_msg.Eventkey == 'good': |
||||
content = random.choices(GOOD_XX)[0] |
||||
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||
result = reply_msg.send() |
||||
elif rec_msg.Event in ['subscribe', 'unsubscribe']: # 订阅 |
||||
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||
result = reply_msg.send() |
||||
if rec_msg.Eventkey == 'qrscene_web.login': # 首次关注,登录认证操作 |
||||
if rec_msg.Event == 'subscribe': |
||||
result = reply_login_msg(rec_msg, to_user, from_user, ) |
||||
if rec_msg.Event == 'unsubscribe': |
||||
ThirdWeChatUserInfo.objects.filter(openid=to_user).update(subscribe=False) |
||||
|
||||
elif rec_msg.Event == 'SCAN': |
||||
if rec_msg.Eventkey == 'web.login': # 已经关注,然后再次扫码,登录认证操作 |
||||
result = reply_login_msg(rec_msg, to_user, from_user, ) |
||||
elif rec_msg.Eventkey.startswith('web.bind.'): |
||||
uid = rec_msg.Eventkey.split('.')[-1] |
||||
wx_user_obj = ThirdWeChatUserInfo.objects.filter(openid=to_user).first() |
||||
user_obj = UserInfo.objects.filter(uid=uid).first() |
||||
update_or_create_wx_userinfo(to_user, user_obj) |
||||
if wx_user_obj: |
||||
if user_obj and user_obj.uid == wx_user_obj.user_id.uid: |
||||
content = f'账户 {wx_user_obj.user_id.first_name} 已经绑定成功,感谢您的使用' |
||||
else: |
||||
content = f'账户已经被 {wx_user_obj.user_id.first_name} 绑定' |
||||
else: |
||||
content = f'账户绑定 {wx_user_obj.user_id.first_name} 成功' |
||||
set_wx_ticket_login_info_cache(rec_msg.Ticket, user_obj.pk) |
||||
reply_msg = reply.TextMsg(to_user, from_user, content) |
||||
result = reply_msg.send() |
||||
|
||||
else: |
||||
logger.error('密文解密失败') |
||||
|
||||
return Response(result) |
||||
|
||||
|
||||
class PageNumber(PageNumberPagination): |
||||
page_size = 10 # 每页显示多少条 |
||||
page_size_query_param = 'size' # URL中每页显示条数的参数 |
||||
page_query_param = 'page' # URL中页码的参数 |
||||
max_page_size = None # 最大页码数限制 |
||||
|
||||
|
||||
class ThirdWxAccount(APIView): |
||||
authentication_classes = [ExpiringTokenAuthentication, ] |
||||
|
||||
def get(self, request): |
||||
res = BaseResponse() |
||||
# wx_open_id = request.query_params.get("openid", None) |
||||
wx_obj_lists = ThirdWeChatUserInfo.objects.filter(user_id=request.user) |
||||
# if wx_open_id: |
||||
# wx_obj_lists = wx_obj_lists.filter(openid=wx_open_id) |
||||
page_obj = PageNumber() |
||||
info_serializer = page_obj.paginate_queryset(queryset=wx_obj_lists.order_by("-subscribe_time"), |
||||
request=request, |
||||
view=self) |
||||
wx_user_info = ThirdWxSerializer(info_serializer, many=True, ) |
||||
res.data = wx_user_info.data |
||||
res.count = wx_obj_lists.count() |
||||
|
||||
return Response(res.dict) |
||||
|
||||
def delete(self, request): |
||||
openid = request.query_params.get("openid") |
||||
user_id = request.query_params.get("user_id") |
||||
if openid and user_id: |
||||
ThirdWeChatUserInfo.objects.filter(user_id_id=user_id, openid=openid).delete() |
||||
return self.get(request) |
Loading…
Reference in new issue