|
|
|
|
@ -1,46 +1,77 @@
|
|
|
|
|
import logging
|
|
|
|
|
import os
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
import jsonpickle
|
|
|
|
|
from django.conf import settings
|
|
|
|
|
from werobot import WeRoBot
|
|
|
|
|
from werobot.replies import ArticlesReply, Article
|
|
|
|
|
from werobot.session.filestorage import FileStorage
|
|
|
|
|
|
|
|
|
|
from djangoblog.utils import get_sha256
|
|
|
|
|
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)
|
|
|
|
|
import logging # 用于记录日志信息
|
|
|
|
|
import os # 用于操作系统相关功能(如环境变量、文件路径)
|
|
|
|
|
import re # 正则表达式,用于文本匹配和提取
|
|
|
|
|
|
|
|
|
|
import jsonpickle # 将 Python 对象序列化为 JSON 字符串(支持复杂对象)
|
|
|
|
|
from django.conf import settings # Django 配置设置,如 BASE_DIR
|
|
|
|
|
from werobot import WeRoBot # WeRoBot 微信机器人框架核心类
|
|
|
|
|
from werobot.replies import ArticlesReply, Article # 构造图文回复消息
|
|
|
|
|
from werobot.session.filestorage import FileStorage # 文件存储会话后端(备用)
|
|
|
|
|
|
|
|
|
|
# 项目内部工具与 API
|
|
|
|
|
from djangoblog.utils import get_sha256 # SHA256 加密函数,用于密码安全
|
|
|
|
|
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', # 微信公众号 Token
|
|
|
|
|
enable_session=True # 启用会话功能,用于维护用户状态
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 创建自定义的缓存会话存储实例(基于 Redis/Memcached)
|
|
|
|
|
memstorage = MemcacheStorage()
|
|
|
|
|
|
|
|
|
|
# 检查缓存存储是否可用
|
|
|
|
|
if memstorage.is_available:
|
|
|
|
|
# 如果可用,使用 MemcacheStorage 作为会话后端
|
|
|
|
|
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)
|
|
|
|
|
# 使用本地文件存储会话数据
|
|
|
|
|
robot.config['SESSION_STORAGE'] = FileStorage(filename='werobot_session')
|
|
|
|
|
|
|
|
|
|
blogapi = BlogApi()
|
|
|
|
|
cmd_handler = CommandHandler()
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
# 实例化博客 API 和命令处理器
|
|
|
|
|
blogapi = BlogApi() # 提供博客文章查询服务
|
|
|
|
|
cmd_handler = CommandHandler() # 提供服务器命令执行功能
|
|
|
|
|
logger = logging.getLogger(__name__) # 获取当前模块的日志记录器
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def convert_to_article_reply(articles, message):
|
|
|
|
|
"""
|
|
|
|
|
将博客文章列表转换为微信图文消息回复格式。
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
articles (list): 包含文章对象的列表(通常来自 blogapi 查询结果)
|
|
|
|
|
message: 当前微信消息对象,用于构造回复
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
ArticlesReply: 可直接返回给用户的图文消息回复对象
|
|
|
|
|
|
|
|
|
|
流程:
|
|
|
|
|
1. 创建一个图文回复对象
|
|
|
|
|
2. 遍历每篇文章,提取标题、摘要、首张图片、链接
|
|
|
|
|
3. 构造 Article 对象并添加到回复中
|
|
|
|
|
"""
|
|
|
|
|
reply = ArticlesReply(message=message)
|
|
|
|
|
from blog.templatetags.blog_tags import truncatechars_content
|
|
|
|
|
from blog.templatetags.blog_tags import truncatechars_content # 导入截断内容的模板标签
|
|
|
|
|
|
|
|
|
|
for post in articles:
|
|
|
|
|
# 使用正则从文章内容中提取第一张图片 URL(png/jpg)
|
|
|
|
|
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),
|
|
|
|
|
description=truncatechars_content(post.body), # 截断内容作为描述
|
|
|
|
|
img=imgurl,
|
|
|
|
|
url=post.get_full_url()
|
|
|
|
|
url=post.get_full_url() # 文章完整 URL
|
|
|
|
|
)
|
|
|
|
|
reply.add_article(article)
|
|
|
|
|
return reply
|
|
|
|
|
@ -48,11 +79,23 @@ def convert_to_article_reply(articles, message):
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r"^\?.*"))
|
|
|
|
|
def search(message, session):
|
|
|
|
|
"""
|
|
|
|
|
处理以 '?' 开头的消息,用于搜索博客文章。
|
|
|
|
|
|
|
|
|
|
示例: "?python" 搜索包含 python 的文章
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
message: 微信消息对象
|
|
|
|
|
session: 当前用户会话对象
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
图文消息 或 文本提示
|
|
|
|
|
"""
|
|
|
|
|
s = message.content
|
|
|
|
|
searchstr = str(s).replace('?', '')
|
|
|
|
|
result = blogapi.search_articles(searchstr)
|
|
|
|
|
searchstr = str(s).replace('?', '') # 去掉问号
|
|
|
|
|
result = blogapi.search_articles(searchstr) # 调用博客 API 搜索
|
|
|
|
|
if result:
|
|
|
|
|
articles = list(map(lambda x: x.object, result))
|
|
|
|
|
articles = list(map(lambda x: x.object, result)) # 提取文章对象
|
|
|
|
|
reply = convert_to_article_reply(articles, message)
|
|
|
|
|
return reply
|
|
|
|
|
else:
|
|
|
|
|
@ -61,13 +104,33 @@ def search(message, session):
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^category\s*$', re.I))
|
|
|
|
|
def category(message, session):
|
|
|
|
|
"""
|
|
|
|
|
处理 "category" 消息,返回所有文章分类目录。
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
message: 微信消息对象
|
|
|
|
|
session: 当前用户会话对象
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
文本消息:列出所有分类名称
|
|
|
|
|
"""
|
|
|
|
|
categorys = blogapi.get_category_lists()
|
|
|
|
|
content = ','.join(map(lambda x: x.name, categorys))
|
|
|
|
|
content = ','.join(map(lambda x: x.name, categorys)) # 拼接分类名
|
|
|
|
|
return '所有文章分类目录:' + content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^recent\s*$', re.I))
|
|
|
|
|
def recents(message, session):
|
|
|
|
|
"""
|
|
|
|
|
处理 "recent" 消息,返回最新发布的文章。
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
message: 微信消息对象
|
|
|
|
|
session: 当前用户会话对象
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
图文消息(最新文章) 或 文本提示
|
|
|
|
|
"""
|
|
|
|
|
articles = blogapi.get_recent_articles()
|
|
|
|
|
if articles:
|
|
|
|
|
reply = convert_to_article_reply(articles, message)
|
|
|
|
|
@ -78,6 +141,16 @@ def recents(message, session):
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile('^help$', re.I))
|
|
|
|
|
def help(message, session):
|
|
|
|
|
"""
|
|
|
|
|
处理 "help" 消息,返回帮助文档。
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
message: 微信消息对象
|
|
|
|
|
session: 当前用户会话对象
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
文本消息:详细的使用说明和命令列表
|
|
|
|
|
"""
|
|
|
|
|
return '''欢迎关注!
|
|
|
|
|
默认会与图灵机器人聊天~~
|
|
|
|
|
你可以通过下面这些命令来获得信息
|
|
|
|
|
@ -100,65 +173,132 @@ def help(message, session):
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^weather\:.*$', re.I))
|
|
|
|
|
def weather(message, session):
|
|
|
|
|
"""
|
|
|
|
|
处理 "weather:" 开头的消息(天气查询功能)。
|
|
|
|
|
当前为占位符,功能正在建设中。
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
message: 微信消息对象
|
|
|
|
|
session: 当前用户会话对象
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
文本消息:提示功能建设中
|
|
|
|
|
"""
|
|
|
|
|
return "建设中..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.filter(re.compile(r'^idcard\:.*$', re.I))
|
|
|
|
|
def idcard(message, session):
|
|
|
|
|
"""
|
|
|
|
|
处理 "idcard:" 开头的消息(身份证信息查询功能)。
|
|
|
|
|
当前为占位符,功能正在建设中。
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
message: 微信消息对象
|
|
|
|
|
session: 当前用户会话对象
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
文本消息:提示功能建设中
|
|
|
|
|
"""
|
|
|
|
|
return "建设中..."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@robot.handler
|
|
|
|
|
def echo(message, session):
|
|
|
|
|
"""
|
|
|
|
|
默认消息处理器,当没有其他 filter 匹配时调用。
|
|
|
|
|
创建 MessageHandler 实例处理消息。
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
message: 微信消息对象
|
|
|
|
|
session: 当前用户会话对象
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
处理结果(文本或图文消息)
|
|
|
|
|
"""
|
|
|
|
|
handler = MessageHandler(message, session)
|
|
|
|
|
return handler.handler()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class MessageHandler:
|
|
|
|
|
"""
|
|
|
|
|
消息处理器类,负责处理用户消息,尤其是管理员命令和认证流程。
|
|
|
|
|
使用会话(session)维护用户状态(是否管理员、是否已认证等)。
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __init__(self, message, session):
|
|
|
|
|
userid = message.source
|
|
|
|
|
self.message = message
|
|
|
|
|
self.session = session
|
|
|
|
|
self.userid = userid
|
|
|
|
|
self.userid = message.source # 用户唯一标识(OpenID)
|
|
|
|
|
|
|
|
|
|
# 尝试从会话中加载用户信息
|
|
|
|
|
try:
|
|
|
|
|
info = session[userid]
|
|
|
|
|
self.userinfo = jsonpickle.decode(info)
|
|
|
|
|
info = session[self.userid]
|
|
|
|
|
self.userinfo = jsonpickle.decode(info) # 反序列化为 WxUserInfo 对象
|
|
|
|
|
except Exception as e:
|
|
|
|
|
# 如果出错(首次访问或会话丢失),创建默认用户信息
|
|
|
|
|
userinfo = WxUserInfo()
|
|
|
|
|
self.userinfo = userinfo
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_admin(self):
|
|
|
|
|
"""判断当前用户是否为管理员"""
|
|
|
|
|
return self.userinfo.isAdmin
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def is_password_set(self):
|
|
|
|
|
"""判断当前管理员是否已完成密码验证"""
|
|
|
|
|
return self.userinfo.isPasswordSet
|
|
|
|
|
|
|
|
|
|
def save_session(self):
|
|
|
|
|
"""
|
|
|
|
|
将当前用户信息保存回会话。
|
|
|
|
|
使用 jsonpickle 序列化对象,并存入 session。
|
|
|
|
|
"""
|
|
|
|
|
info = jsonpickle.encode(self.userinfo)
|
|
|
|
|
self.session[self.userid] = info
|
|
|
|
|
|
|
|
|
|
def handler(self):
|
|
|
|
|
"""
|
|
|
|
|
核心消息处理逻辑,根据用户状态和输入内容返回相应响应。
|
|
|
|
|
|
|
|
|
|
处理流程:
|
|
|
|
|
1. 管理员退出登录
|
|
|
|
|
2. 管理员登录请求
|
|
|
|
|
3. 管理员密码验证
|
|
|
|
|
4. 执行管理员命令
|
|
|
|
|
5. 默认:调用 ChatGPT 进行聊天
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
str: 要回复给用户的消息内容
|
|
|
|
|
"""
|
|
|
|
|
info = self.message.content
|
|
|
|
|
|
|
|
|
|
# 退出管理员模式
|
|
|
|
|
if self.userinfo.isAdmin and info.upper() == 'EXIT':
|
|
|
|
|
self.userinfo = WxUserInfo()
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "退出成功"
|
|
|
|
|
|
|
|
|
|
# 请求进入管理员模式
|
|
|
|
|
if info.upper() == 'ADMIN':
|
|
|
|
|
self.userinfo.isAdmin = True
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "输入管理员密码"
|
|
|
|
|
|
|
|
|
|
# 管理员密码验证阶段
|
|
|
|
|
if self.userinfo.isAdmin and not self.userinfo.isPasswordSet:
|
|
|
|
|
# 获取配置中的管理员密码(SHA256 加密后)
|
|
|
|
|
passwd = settings.WXADMIN
|
|
|
|
|
if settings.TESTING:
|
|
|
|
|
passwd = '123'
|
|
|
|
|
passwd = '123' # 测试环境下使用简单密码
|
|
|
|
|
# 验证用户输入的密码(双重 SHA256)
|
|
|
|
|
if passwd.upper() == get_sha256(get_sha256(info)).upper():
|
|
|
|
|
self.userinfo.isPasswordSet = True
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "验证通过,请输入命令或者要执行的命令代码:输入helpme获得帮助"
|
|
|
|
|
else:
|
|
|
|
|
# 密码错误次数限制
|
|
|
|
|
if self.userinfo.Count >= 3:
|
|
|
|
|
self.userinfo = WxUserInfo()
|
|
|
|
|
self.save_session()
|
|
|
|
|
@ -166,22 +306,33 @@ class MessageHandler:
|
|
|
|
|
self.userinfo.Count += 1
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "验证失败,请重新输入管理员密码:"
|
|
|
|
|
|
|
|
|
|
# 管理员已认证,可执行命令
|
|
|
|
|
if self.userinfo.isAdmin and self.userinfo.isPasswordSet:
|
|
|
|
|
if self.userinfo.Command != '' and info.upper() == 'Y':
|
|
|
|
|
# 用户确认执行命令
|
|
|
|
|
return cmd_handler.run(self.userinfo.Command)
|
|
|
|
|
else:
|
|
|
|
|
if info.upper() == 'HELPME':
|
|
|
|
|
# 显示命令帮助
|
|
|
|
|
return cmd_handler.get_help()
|
|
|
|
|
# 记录待执行的命令,等待用户确认
|
|
|
|
|
self.userinfo.Command = info
|
|
|
|
|
self.save_session()
|
|
|
|
|
return "确认执行: " + info + " 命令?"
|
|
|
|
|
|
|
|
|
|
# 默认行为:调用 ChatGPT 进行普通聊天
|
|
|
|
|
return ChatGPT.chat(info)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 # 密码尝试次数
|
|
|
|
|
self.Command = '' # 待执行的命令
|