|
|
|
|
@ -13,175 +13,222 @@ from servermanager.api.blogapi import BlogApi
|
|
|
|
|
from servermanager.api.commonapi import ChatGPT, CommandHandler
|
|
|
|
|
from .MemcacheStorage import MemcacheStorage
|
|
|
|
|
|
|
|
|
|
robot = WeRoBot(token=os.environ.get('DJANGO_WEROBOT_TOKEN')
|
|
|
|
|
or 'lylinux', enable_session=True)
|
|
|
|
|
# 初始化微信机器人(WeRoBot)
|
|
|
|
|
# 从环境变量获取Token,默认值为'lylinux';启用会话功能以保存用户状态
|
|
|
|
|
robot = WeRoBot(
|
|
|
|
|
token=os.environ.get('DJANGO_WEROBOT_TOKEN') or 'lylinux',
|
|
|
|
|
enable_session=True
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 配置会话存储:优先使用Memcache,失败则降级为文件存储
|
|
|
|
|
memstorage = MemcacheStorage()
|
|
|
|
|
if memstorage.is_available:
|
|
|
|
|
if memstorage.is_available: # 检查Memcache是否可用
|
|
|
|
|
robot.config['SESSION_STORAGE'] = memstorage
|
|
|
|
|
else:
|
|
|
|
|
if os.path.exists(os.path.join(settings.BASE_DIR, 'werobot_session')):
|
|
|
|
|
os.remove(os.path.join(settings.BASE_DIR, 'werobot_session'))
|
|
|
|
|
# 清理旧的文件存储,避免残留数据
|
|
|
|
|
session_file = os.path.join(settings.BASE_DIR, 'werobot_session')
|
|
|
|
|
if os.path.exists(session_file):
|
|
|
|
|
os.remove(session_file)
|
|
|
|
|
# 使用文件存储会话数据(适合开发或Memcache不可用场景)
|
|
|
|
|
robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session')
|
|
|
|
|
|
|
|
|
|
blogapi = BlogApi()
|
|
|
|
|
cmd_handler = CommandHandler()
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
# 初始化依赖组件
|
|
|
|
|
blogapi = BlogApi() # 博客数据接口(文章搜索、分类查询等)
|
|
|
|
|
cmd_handler = CommandHandler() # 系统命令处理(执行预设命令)
|
|
|
|
|
logger = logging.getLogger(__name__) # 日志记录器
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def convert_to_article_reply(articles, message):
|
|
|
|
|
"""
|
|
|
|
|
将文章列表转换为微信公众号的“图文消息”回复格式
|
|
|
|
|
:param articles: 文章对象列表
|
|
|
|
|
:param message: 微信接收的消息对象(用于构建回复)
|
|
|
|
|
:return: ArticlesReply 图文回复对象
|
|
|
|
|
"""
|
|
|
|
|
reply = ArticlesReply(message=message)
|
|
|
|
|
# 导入自定义模板标签,用于截取文章内容作为描述
|
|
|
|
|
from blog.templatetags.blog_tags import truncatechars_content
|
|
|
|
|
|
|
|
|
|
for post in articles:
|
|
|
|
|
# 正则提取文章正文中的第一张图片(作为图文消息封面)
|
|
|
|
|
imgs = re.findall(r'(?:http\:|https\:)?\/\/.*\.(?:png|jpg)', post.body)
|
|
|
|
|
imgurl = ''
|
|
|
|
|
if imgs:
|
|
|
|
|
imgurl = imgs[0]
|
|
|
|
|
imgurl = imgs[0] if imgs else '' # 无图片则用空字符串
|
|
|
|
|
|
|
|
|
|
# 构建单条图文信息
|
|
|
|
|
article = Article(
|
|
|
|
|
title=post.title,
|
|
|
|
|
description=truncatechars_content(post.body),
|
|
|
|
|
img=imgurl,
|
|
|
|
|
url=post.get_full_url()
|
|
|
|
|
title=post.title, # 文章标题
|
|
|
|
|
description=truncatechars_content(post.body), # 截取内容作为描述
|
|
|
|
|
img=imgurl, # 封面图片URL
|
|
|
|
|
url=post.get_full_url() # 文章详情页URL
|
|
|
|
|
)
|
|
|
|
|
reply.add_article(article)
|
|
|
|
|
reply.add_article(article) # 添加到图文回复中
|
|
|
|
|
return reply
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r"^\?.*"))
|
|
|
|
|
# ------------------------------ 微信消息处理过滤器 ------------------------------
|
|
|
|
|
@robot.filter(re.compile(r"^\?.*")) # 匹配以"?"开头的消息(文章搜索)
|
|
|
|
|
def search(message, session):
|
|
|
|
|
s = message.content
|
|
|
|
|
searchstr = str(s).replace('?', '')
|
|
|
|
|
result = blogapi.search_articles(searchstr)
|
|
|
|
|
"""处理文章搜索:输入“?关键词”返回匹配的图文消息"""
|
|
|
|
|
searchstr = message.content.replace('?', '') # 提取关键词(去除开头的"?")
|
|
|
|
|
result = blogapi.search_articles(searchstr) # 调用博客接口搜索文章
|
|
|
|
|
|
|
|
|
|
if result:
|
|
|
|
|
articles = list(map(lambda x: x.object, result))
|
|
|
|
|
reply = convert_to_article_reply(articles, message)
|
|
|
|
|
return reply
|
|
|
|
|
# 将搜索结果(SearchQuerySet)转换为文章对象列表
|
|
|
|
|
articles = [x.object for x in result]
|
|
|
|
|
return convert_to_article_reply(articles, message) # 返回图文回复
|
|
|
|
|
else:
|
|
|
|
|
return '没有找到相关文章。'
|
|
|
|
|
return '没有找到相关文章。' # 无结果提示
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^category\s*$', re.I))
|
|
|
|
|
@robot.filter(re.compile(r'^category\s*$', re.I)) # 匹配"category"(不区分大小写)
|
|
|
|
|
def category(message, session):
|
|
|
|
|
categorys = blogapi.get_category_lists()
|
|
|
|
|
content = ','.join(map(lambda x: x.name, categorys))
|
|
|
|
|
return '所有文章分类目录:' + content
|
|
|
|
|
"""获取所有文章分类:输入“category”返回分类列表"""
|
|
|
|
|
categorys = blogapi.get_category_lists() # 调用接口获取所有分类
|
|
|
|
|
# 拼接分类名称为字符串(如“Python,Java,前端”)
|
|
|
|
|
category_names = ','.join([x.name for x in categorys])
|
|
|
|
|
return f'所有文章分类目录:{category_names}'
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^recent\s*$', re.I))
|
|
|
|
|
@robot.filter(re.compile(r'^recent\s*$', re.I)) # 匹配"recent"(不区分大小写)
|
|
|
|
|
def recents(message, session):
|
|
|
|
|
articles = blogapi.get_recent_articles()
|
|
|
|
|
"""获取最新文章:输入“recent”返回最新文章的图文消息"""
|
|
|
|
|
articles = blogapi.get_recent_articles() # 调用接口获取最新文章
|
|
|
|
|
|
|
|
|
|
if articles:
|
|
|
|
|
reply = convert_to_article_reply(articles, message)
|
|
|
|
|
return reply
|
|
|
|
|
return convert_to_article_reply(articles, message) # 返回图文回复
|
|
|
|
|
else:
|
|
|
|
|
return "暂时还没有文章"
|
|
|
|
|
return "暂时还没有文章" # 无文章提示
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile('^help$', re.I))
|
|
|
|
|
@robot.filter(re.compile('^help$', re.I)) # 匹配"help"(不区分大小写)
|
|
|
|
|
def help(message, session):
|
|
|
|
|
"""获取帮助:输入“help”返回功能说明"""
|
|
|
|
|
return '''欢迎关注!
|
|
|
|
|
默认会与图灵机器人聊天~~
|
|
|
|
|
你可以通过下面这些命令来获得信息
|
|
|
|
|
?关键字搜索文章.
|
|
|
|
|
如?python.
|
|
|
|
|
category获得文章分类目录及文章数.
|
|
|
|
|
category-***获得该分类目录文章
|
|
|
|
|
如category-python
|
|
|
|
|
recent获得最新文章
|
|
|
|
|
help获得帮助.
|
|
|
|
|
weather:获得天气
|
|
|
|
|
如weather:西安
|
|
|
|
|
idcard:获得身份证信息
|
|
|
|
|
如idcard:61048119xxxxxxxxxx
|
|
|
|
|
music:音乐搜索
|
|
|
|
|
如music:阴天快乐
|
|
|
|
|
PS:以上标点符号都不支持中文标点~~
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^weather\:.*$', re.I))
|
|
|
|
|
默认会与图灵机器人聊天~~
|
|
|
|
|
你可以通过下面这些命令来获得信息:
|
|
|
|
|
1. ?关键字 → 搜索文章(如?python)
|
|
|
|
|
2. category → 获得文章分类目录
|
|
|
|
|
3. category-*** → 获得该分类下的文章(如category-python)
|
|
|
|
|
4. recent → 获得最新文章
|
|
|
|
|
5. help → 获得帮助
|
|
|
|
|
6. weather:城市 → 获得天气(如weather:西安)
|
|
|
|
|
7. idcard:号码 → 获得身份证信息(如idcard:61048119xxxxxxxxxx)
|
|
|
|
|
8. music:歌名 → 音乐搜索(如music:阴天快乐)
|
|
|
|
|
|
|
|
|
|
PS: 以上标点符号不支持中文标点~~
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^weather\:.*$', re.I)) # 匹配"weather:城市"格式
|
|
|
|
|
def weather(message, session):
|
|
|
|
|
"""天气查询(待开发):返回“建设中”提示"""
|
|
|
|
|
return "建设中..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^idcard\:.*$', re.I))
|
|
|
|
|
@robot.filter(re.compile(r'^idcard\:.*$', re.I)) # 匹配"idcard:号码"格式
|
|
|
|
|
def idcard(message, session):
|
|
|
|
|
"""身份证信息查询(待开发):返回“建设中”提示"""
|
|
|
|
|
return "建设中..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.handler
|
|
|
|
|
# ------------------------------ 默认消息处理器 ------------------------------
|
|
|
|
|
@robot.handler # 未被上述过滤器匹配的消息,进入此默认处理器
|
|
|
|
|
def echo(message, session):
|
|
|
|
|
"""默认消息处理:转发给MessageHandler处理(用户状态管理、管理员命令等)"""
|
|
|
|
|
handler = MessageHandler(message, session)
|
|
|
|
|
return handler.handler()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------ 用户状态与命令管理 ------------------------------
|
|
|
|
|
class MessageHandler:
|
|
|
|
|
"""处理用户消息的核心类:管理用户状态(普通用户/管理员)、执行管理员命令"""
|
|
|
|
|
def __init__(self, message, session):
|
|
|
|
|
userid = message.source
|
|
|
|
|
self.message = message
|
|
|
|
|
self.session = session
|
|
|
|
|
self.userid = userid
|
|
|
|
|
self.message = message # 微信消息对象
|
|
|
|
|
self.session = session # 会话存储(保存用户状态)
|
|
|
|
|
self.userid = message.source # 用户唯一标识(微信OpenID)
|
|
|
|
|
|
|
|
|
|
# 从会话中加载用户状态(用jsonpickle反序列化)
|
|
|
|
|
try:
|
|
|
|
|
info = session[userid]
|
|
|
|
|
self.userinfo = jsonpickle.decode(info)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
userinfo = WxUserInfo()
|
|
|
|
|
self.userinfo = userinfo
|
|
|
|
|
user_info_json = session[self.userid]
|
|
|
|
|
self.userinfo = jsonpickle.decode(user_info_json)
|
|
|
|
|
except Exception:
|
|
|
|
|
# 会话中无用户状态,初始化新的用户信息
|
|
|
|
|
self.userinfo = WxUserInfo()
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_admin(self):
|
|
|
|
|
"""判断当前用户是否处于“管理员模式”"""
|
|
|
|
|
return self.userinfo.isAdmin
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_password_set(self):
|
|
|
|
|
"""判断管理员是否已通过密码验证"""
|
|
|
|
|
return self.userinfo.isPasswordSet
|
|
|
|
|
|
|
|
|
|
def save_session(self):
|
|
|
|
|
info = jsonpickle.encode(self.userinfo)
|
|
|
|
|
self.session[self.userid] = info
|
|
|
|
|
"""将用户状态序列化后保存到会话"""
|
|
|
|
|
user_info_json = jsonpickle.encode(self.userinfo)
|
|
|
|
|
self.session[self.userid] = user_info_json
|
|
|
|
|
|
|
|
|
|
def handler(self):
|
|
|
|
|
info = self.message.content
|
|
|
|
|
"""核心处理逻辑:根据用户状态分发消息处理"""
|
|
|
|
|
user_input = self.message.content # 用户输入的内容
|
|
|
|
|
|
|
|
|
|
if self.userinfo.isAdmin and info.upper() == 'EXIT':
|
|
|
|
|
self.userinfo = WxUserInfo()
|
|
|
|
|
# 1. 管理员退出:已验证的管理员输入“EXIT”,退出管理员模式
|
|
|
|
|
if self.is_admin and self.is_password_set and user_input.upper() == 'EXIT':
|
|
|
|
|
self.userinfo = WxUserInfo() # 重置用户状态为普通用户
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "退出成功"
|
|
|
|
|
if info.upper() == 'ADMIN':
|
|
|
|
|
self.userinfo.isAdmin = True
|
|
|
|
|
|
|
|
|
|
# 2. 进入管理员模式:普通用户输入“ADMIN”,触发管理员验证流程
|
|
|
|
|
if user_input.upper() == 'ADMIN' and not self.is_admin:
|
|
|
|
|
self.userinfo.isAdmin = True # 标记为管理员模式
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "输入管理员密码"
|
|
|
|
|
if self.userinfo.isAdmin and not self.userinfo.isPasswordSet:
|
|
|
|
|
passwd = settings.WXADMIN
|
|
|
|
|
if settings.TESTING:
|
|
|
|
|
passwd = '123'
|
|
|
|
|
if passwd.upper() == get_sha256(get_sha256(info)).upper():
|
|
|
|
|
self.userinfo.isPasswordSet = True
|
|
|
|
|
|
|
|
|
|
# 3. 管理员密码验证:处于管理员模式但未验证密码
|
|
|
|
|
if self.is_admin and not self.is_password_set:
|
|
|
|
|
# 获取正确密码(测试环境用'123',正式环境用settings中的WXADMIN)
|
|
|
|
|
correct_passwd = '123' if settings.TESTING else settings.WXADMIN
|
|
|
|
|
# 密码加密比对(两次SHA256加密,避免明文传输风险)
|
|
|
|
|
input_passwd = get_sha256(get_sha256(user_input)).upper()
|
|
|
|
|
|
|
|
|
|
if input_passwd == correct_passwd.upper():
|
|
|
|
|
self.userinfo.isPasswordSet = True # 标记为已验证
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助"
|
|
|
|
|
return "验证通过,请输入命令或执行代码:输入helpme获得帮助"
|
|
|
|
|
else:
|
|
|
|
|
# 密码错误次数限制(3次后重置管理员模式)
|
|
|
|
|
self.userinfo.Count += 1
|
|
|
|
|
if self.userinfo.Count >= 3:
|
|
|
|
|
self.userinfo = WxUserInfo()
|
|
|
|
|
self.userinfo = WxUserInfo() # 重置为普通用户
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "超过验证次数"
|
|
|
|
|
self.userinfo.Count += 1
|
|
|
|
|
return "超过验证次数,已退出管理员模式"
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "验证失败,请重新输入管理员密码:"
|
|
|
|
|
if self.userinfo.isAdmin and self.userinfo.isPasswordSet:
|
|
|
|
|
if self.userinfo.Command != '' and info.upper() == 'Y':
|
|
|
|
|
return f"验证失败(剩余{3 - self.userinfo.Count}次),请重新输入管理员密码:"
|
|
|
|
|
|
|
|
|
|
# 4. 管理员命令执行:已验证的管理员输入命令
|
|
|
|
|
if self.is_admin and self.is_password_set:
|
|
|
|
|
# 确认执行命令:若之前已输入命令且当前输入“Y”,则执行
|
|
|
|
|
if self.userinfo.Command != '' and user_input.upper() == 'Y':
|
|
|
|
|
return cmd_handler.run(self.userinfo.Command)
|
|
|
|
|
# 查看帮助:输入“helpme”返回命令列表
|
|
|
|
|
elif user_input.upper() == 'HELPME':
|
|
|
|
|
return cmd_handler.get_help()
|
|
|
|
|
# 暂存命令:输入新命令,提示确认
|
|
|
|
|
else:
|
|
|
|
|
if info.upper() == 'HELPME':
|
|
|
|
|
return cmd_handler.get_help()
|
|
|
|
|
self.userinfo.Command = info
|
|
|
|
|
self.userinfo.Command = user_input
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "确认执行: " + info + " 命令?"
|
|
|
|
|
return f"确认执行命令:{user_input}?(输入Y执行)"
|
|
|
|
|
|
|
|
|
|
return ChatGPT.chat(info)
|
|
|
|
|
# 5. 普通用户:默认转发给ChatGPT生成回复
|
|
|
|
|
return ChatGPT.chat(user_input)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WxUserInfo():
|
|
|
|
|
class WxUserInfo:
|
|
|
|
|
"""用户状态类:存储用户是否为管理员、密码验证状态、命令暂存等信息"""
|
|
|
|
|
def __init__(self):
|
|
|
|
|
self.isAdmin = False
|
|
|
|
|
self.isPasswordSet = False
|
|
|
|
|
self.Count = 0
|
|
|
|
|
self.Command = ''
|
|
|
|
|
self.isAdmin = False # 是否处于管理员模式(默认否)
|
|
|
|
|
self.isPasswordSet = False # 是否已通过密码验证(默认否)
|
|
|
|
|
self.Count = 0 # 密码错误次数(默认0)
|
|
|
|
|
self.Command = '' # 暂存的管理员命令(默认空)
|
|
|
|
|
|