教程万字长文保姆级教你制作自己的多功能QQ机器人
Posted 小锋学长生活大爆炸
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了教程万字长文保姆级教你制作自己的多功能QQ机器人相关的知识,希望对你有一定的参考价值。
转载请注明出处:小锋学长生活大爆炸( https://xfxuezhang.blog.csdn.net/)
若发现存在部分图片缺失,可以访问原文: 万字长文保姆级教你制作自己的多功能QQ机器人 - 小锋学长生活大爆炸
目录
前言
QQ、微信是我们平常使用最多的通讯工具,网上也有很多通过软件去控制QQ/微信的开源工具,通过这些工具,我们可以实现许多有意思的效果,而不仅仅局限于消息聊天。
自从微信网页版被官方禁用后,微信的软件工具几乎已经失效了,现有的一些是通过hook微信本身来实现,这种很容易被官方检测并封号。另一些是通过注册企业号来控制,但不直观且功能受限。
这里我们借助相对更开放的QQ来制作我们的机器人,对比几款工具后,最终选择了mirai。
先放一张整体结构图:
功能清单
网上现有开源的机器人大多只是实现了类似“自动推送天气、接入图灵机器人自动聊天”等等,大多属于自娱自乐,没有发挥最大用途。
因此,我们的QQ机器人(暂且取名为“小锋仔”)是根据日常所需而制,包含常用功能且设计得易于扩展。
目前包含的功能有:
- 类似QMsg酱的消息通知
- QQ群消息转发
- 翻译查询
- 照片上传
- 实况天气
- 实时热搜
- 控制树莓派舵机
- 控制树莓派屏显
- ... ...
将来可能包含的功能有:
- 接入控制ESP32(实现智能家居控制)
接下来详细介绍如何自己搭建一个这样的QQ机器人。篇幅较长且保姆级详细,建议收藏后慢慢看。
免费领取轻量应用云服务器
首先为了 能运行mirai,且随时随地能连接,我们需要有一个 具备公网IP的服务器。这里使用腾讯云的 免费服务器。
对于还不想买的童靴,可以免费领取腾讯云提供的1个月服务器试用套餐。步骤:
- 进入官网领取:云产品免费试用;需要选购的进:轻量应用服务器专场 (限时3年388,点我进);不清楚怎么操作的可以看教程:腾讯云产品免费试用教程
- 领取完成后,由于后面需要用到端口,因此这里我们提前开放2个端口:8888和9966
这里腾讯云可能有个小特点。如果发现在控制台防火墙放行后,还是无法访问。需要再在服务器里放行一下端口。这里先写着,大家可以在后面一节中连接上了服务器,再回过来这里输入指令。
sudo apt install firewalld -y
sudo firewall-cmd --list-all
sudo firewall-cmd --permanent --zone=public --add-port=8888/tcp && sudo firewall-cmd --reload
sudo firewall-cmd --permanent --zone=public --add-port=9966/tcp && sudo firewall-cmd --reload
sudo systemctl start firewalld.service
SSH连接服务器
服务器初始化完成后,就可以通过SSH去连接了。这里我们可以直接使用powershell来连接,其他SSH软件我强推mobaxterm!!安装包也已经准备好了:MobaXterm.exe
- 搜索打开powershell:
- 输入以下命令连接SSH:
ssh 用户名@<公网ip>
- 或者使用MobaXterm软件:
- 先更新一下软件库:
sudo apt upgrade -y
sudo apt autoremove -y
- 一般不建议使用管理员账户,因此我们要自己新建一个账户:
sudo adduser sxf
然后将账户加入sudoers组:
sudo apt install vim
sudo vim /etc/sudoers
然后退出软件,重新用新建的账号登录即可。
至此,服务器环境就搭建完成了。
常见Ubuntu软件安装与问题修复
这篇博客里记录了很多我在使用过程中,常用软件的安装,非常详细且经过亲测,时不时也会更新内容,大家可以收藏以备下次使用。
Ubuntu20.04 + VirtualBox相关_小锋学长生活大爆炸的博客-CSDN博客
搭建mirai环境
接下来就要在服务器上搭建QQ机器人(mirai)基础环境。搭建完成后,我们就可以远程跟机器人进行交互。
官方mirai的github仓库:GitHub - mamoe/mirai: 高效率 QQ 机器人支持库
由于github是国外的,而官方已经不再支持gitee的维护,因此如果大家无法访问上面的连接,可以用我帮大家下载下来的安装包:
其他的一些文档:Mirai | mirai
官方论坛:主页 | MiraiForum
下面开始正式安装:
- 先SSH连接上服务器,建议不要用root用户登录。
- 下载安装包mcl-installer-a02f711-linux-amd64:
mkdir qqbot
cd qqbot
wget http://xfxuezhang.cn/web/share/QQBot/mcl-installer-a02f711-linux-amd64
sudo chmod +x mcl-installer-a02f711-linux-amd64
此时需要输入密码(在上面选购并装完服务器后会显示,当时要求记下的)。
./mcl-installer-a02f711-linux-amd64
此时进入安装流程,弹出的几个选项都直接回车选默认即可。
- 安装完成后,还需要安装mirai-api-http。在当前页面下,继续输入:
./mcl --update-package net.mamoe:mirai-api-http --channel stable-v2 --type plugin
- 编辑_config/net.mamoe.mirai-api-http/setting.yml_配置文件 (没有则自行创建)
## 配置文件中的值,全为默认值
## 启用的 adapter, 内置有 http, ws, reverse-ws, webhook
adapters:
- http
- ws
## 是否开启认证流程, 若为 true 则建立连接时需要验证 verifyKey
## 建议公网连接时开启
enableVerify: true
verifyKey: 1234567890
## 开启一些调式信息
debug: false
## 是否开启单 session 模式, 若为 true,则自动创建 session 绑定 console 中登录的 bot
## 开启后,接口中任何 sessionKey 不需要传递参数
## 若 console 中有多个 bot 登录,则行为未定义
## 确保 console 中只有一个 bot 登陆时启用
singleMode: false
## 历史消息的缓存大小
## 同时,也是 http adapter 的消息队列容量
cacheSize: 4096
## adapter 的单独配置,键名与 adapters 项配置相同
adapterSettings:
## 详情看 http adapter 使用说明 配置
http:
# 0.0.0.0是允许远程访问,localhost只能同机器访问
host: 0.0.0.0
port: 8888
cors: ["*"]
unreadQueueMaxSize: 100
## 详情看 websocket adapter 使用说明 配置
ws:
host: localhost
port: 8080
reservedSyncId: -1
- 启动mirai即可:
./mcl
首次启动会自动下载jar包。等待启动完成后,输入"?",可以查看所有支持的mcl命令。
- 使用以下命令即可登录QQ号:
/login [password]
如果想要启动mcl后自动登录QQ号,可以用:
/autoLogin add
也可以设置不同的设备登录。
/autoLogin setConfig protocol android_PAD
它对应的配置文件其实就在:config/Console/AutoLogin.yml
-
现在QQ风控很严了,第一次登录很有可能遇到“需要滑动验证码”的。建议申请小号使用,以免发生不测。并且首次使用时在QQ“账号安全设置”中关闭“安全登录检查”、“陌生设备登录保护”。如果遇到验证码,可以尝试:
- 将Captcha link通过另一个QQ,发给待登录的mirai-QQ,手机登录mirai-QQ并点击链接,手动完成滑块验证,然后回到mobaxterm输入回车;
- 如果不行,就参考这个链接的方法:GitHub - project-mirai/mirai-login-solver-selenium: SliderCaptcha solver;
- 还不行,再参考这个链接的方法:mirai-login-solver-selenium | mirai;
- 还有一个小技巧可以尝试。在手机端先通过手机号登录QQ,如果没问题,再通过手机号在mirai上登录。手机建议先登录上mirai-QQ,有时可能会弹窗提示“是否允许陌生设备登录”等等,要手动点确认的。
- 另外,最新申请的QQ号,一般可以成功登录mirai。
- 如果以上都不行,目前的终极方案是使用miraiAndroid:MiraiAndroid:
- 在手机上的MiraiAndroid登录QQ后导出device.json
- 将cache目录下的3个文件account.secrets、servers.json、session.bin也复制出来
- 接下来点击左上角, 再点击“工具”。选择你机器人的账号, 选择 导出 DEVICE.JSON 将其导出。
- 再次回到服务器端,进入 “bots/<你的QQ号>” 下面, 将导出的 device.json 复制放入。对应的cache文件夹也复制放入。
- 再次执行 ./mcl 启动 mirai-console 看看效果。
- 若仍有问题,欢迎加入文末Q群交流。
- 至此,mcl就已经能正常接收QQ消息了。而我们的实现代码对mcl的控制,就是通过mirai-api-http插件来实现的。根据上面第4步配置的_setting.yml_文件,再参考官方API文档和HttpAdapter文档,即可实现互联互通。(讲起来比较麻烦,no bibi,后面直接show me the code)。
Python控制mirai篇
当服务器成功运行了mirai后,我们就可以在本地进行Python脚本的编写了。由于最新的mirai-api-http变更过接口规范,因此网上某些一两年前的代码已经失效了。本教程对应的mirai-api-http使用的是最新的2.x版本。
接下来的操作,都默认已经完成“启动mcl并login了QQ号”。
在上面setting.yml中,有两个配置项值得注意,他是我们脚本可以控制的密钥:
verifyKey: 1234567890
http: port: 8888
debug输出封装
简单封装下。直接用print也是可以的。
class Logger:
def __init__(self, level='debug'):
self.level = level
def DebugLog(self, *args):
if self.level == 'debug':
print(*args)
def TraceLog(self, *args):
if self.level == 'trace':
print(*args)
def setDebugLevel(self, level):
self.level = level.lower()
交互授权
在交互前,脚本需要先向mirai获取一个verifyKey,之后在每个请求时候,都需要带上这个key,也叫session。其中,参数auth_key对应了上面setting.yml里的verifyKey。
auth_key = '1234567890'
def verifySession(self, auth_key):
"""每个Session只能绑定一个Bot,但一个Bot可有多个Session。
session Key在未进行校验的情况下,一定时间后将会被自动释放"""
data = "verifyKey": auth_key
url = self.addr+'verify'
res = requests.post(url, data=json.dumps(data)).json()
logger.DebugLog(res)
if res['code'] == 0:
return res['session']
return None
绑定bot
使用此方法校验并激活你的Session,同时将Session与一个已登录的Bot绑定。
qq = '121215' # mirai登录的那个QQ
session = 'grge8484' # 上面verifySession函数的返回值
def bindSession(self, session, qq):
"""校验并激活Session,同时将Session与一个已登录的Bot绑定"""
data = "sessionKey": session, "qq": qq
url = self.addr + 'bind'
res = requests.post(url, data=json.dumps(data)).json()
logger.DebugLog(res)
if res['code'] == 0:
self.session = session
return True
return False
释放bot
使用此方式释放session及其相关资源(Bot不会被释放)
def releaseSession(self, session, qq):
"""不使用的Session应当被释放,长时间(30分钟)未使用的Session将自动释放,
否则Session持续保存Bot收到的消息,将会导致内存泄露(开启websocket后将不会自动释放)"""
data = "sessionKey": session, "qq": qq
url = self.addr + 'release'
res = requests.post(url, data=json.dumps(data)).json()
logger.DebugLog(res)
if res['code'] == 0:
return True
return False
未读消息的数量
获取当前有多少条未读消息。
def getMessageCount(self, session):
url = self.addr + 'countMessage?sessionKey='+session
res = requests.get(url).json()
if res['code'] == 0:
return res['data']
return 0
获取最新的消息
获取消息后会从队列中移除。
def fetchLatestMessage(self, session):
url = self.addr + 'fetchLatestMessage?count=10&sessionKey='+session
res = requests.get(url).json()
if res['code'] == 0:
return res['data']
return None
解析消息内容
简单实现了部分消息类型的解析,会有消息丢失,请根据使用需求自行调整。
data = 'xxx' # 可以是上面getMsgFromGroup函数的返回值
def parseGroupMsg(self, data):
res = []
if data is None:
return res
for item in data:
if item['type'] == 'GroupMessage':
type = item['messageChain'][-1]['type']
if type == 'Image':
text = item['messageChain'][-1]['url']
elif type == 'Plain':
text = item['messageChain'][-1]['text']
elif type == 'Face':
text = item['messageChain'][-1]['faceId']
else:
logger.TraceLog(">> 当前消息类型暂不支持转发:=> "+type)
continue
name = item['sender']['memberName']
group_id = str(item['sender']['group']['id'])
group_name = item['sender']['group']['name']
res.append('text': text, 'type': type, 'name': name, 'groupId': group_id, 'groupName': group_name)
return res
向好友发送消息
向指定好友发送消息。
def sendFriendMessage(self, session, qq, msg):
msg_list = msg.split(r'\\n')
msg_chain = [ "type": "Plain", "text": m+'\\n' for m in msg_list]
data =
"sessionKey": session,
"target": qq,
"messageChain": msg_chain
url = self.addr + 'sendFriendMessage'
try:
res = requests.post(url, data=json.dumps(data)).json()
except:
logger.DebugLog(">> 发送失败")
return 0
if res['code'] == 0:
return res['messageId']
return 0
向群发送消息
也只是简单实现。
def sendMsgToGroup(self, session, group, msg):
text = msg['text']
type = msg['type']
name = msg['name']
group_id = msg['groupId']
group_name = msg['groupName']
content1 = "【消息中转助手】\\n用户:\\n群号:\\n群名:\\n消息:\\n".format(
name, group_id, group_name, text)
content2 = "【消息中转助手】\\n用户:\\n群号:\\n群名:\\n消息:\\n".format(
name, group_id, group_name)
logger.DebugLog(">> 消息类型:" + type)
if type == 'Plain':
message = ["type": type, "text": content1]
elif type == 'Image':
message = [
"type": 'Plain', "text": content2,
"type": type, "url": text]
elif type == 'Face':
message = ["type": 'Plain', "text": content2,
"type": type, "faceId": text]
else:
logger.TraceLog(">> 当前消息类型暂不支持转发:=> "+type)
return 0
data =
"sessionKey": session,
"group": group,
"messageChain": message
logger.DebugLog(">> 消息内容:" + str(data))
url = self.addr + 'sendGroupMessage'
try:
res = requests.post(url, data=json.dumps(data)).json()
except:
logger.DebugLog(">> 转发失败")
return 0
logger.DebugLog(">> 请求返回:" + str(res))
if res['code'] == 0:
return res['messageId']
return 0
向群发送富文本消息
跟上面的差不多,消息类型变了一下,从而支持类似html形式的消息发送。
def sendPlainTextToGroup(self, session, group, msg):
msg_list = msg.split(r'\\n')
msg_chain = [ "type": "Plain", "text": m+'\\n' for m in msg_list]
data =
"sessionKey": session,
"group": group,
"messageChain": msg_chain
url = self.addr + 'sendGroupMessage'
try:
res = requests.post(url, data=json.dumps(data)).json()
except:
logger.DebugLog(">> 转发失败")
return 0
logger.DebugLog(">> 请求返回:" + str(res))
if res['code'] == 0:
return res['messageId']
return 0
以上就是几个简单、常用的函数。基于这些函数,就已经可以实现蛮多有趣的功能了。
Q群消息转发
这部分可以直接参考之前的博客:Q群消息转发例程。其实也就是把上面的函数整合一下,放一个完整版:
import requests
from time import sleep
class Logger:
def __init__(self, level='debug'):
self.level = level
def DebugLog(self, *args):
if self.level == 'debug':
print(*args)
def TraceLog(self, *args):
if self.level == 'trace':
print(*args)
def setDebugLevel(self, level):
self.level = level.lower()
logger = Logger()
class QQBot:
def __init__(self):
self.addr = 'http://43.143.12.250:8888/'
self.session = None
def verifySession(self, auth_key):
"""每个Session只能绑定一个Bot,但一个Bot可有多个Session。
session Key在未进行校验的情况下,一定时间后将会被自动释放"""
data = "verifyKey": auth_key
url = self.addr+'verify'
res = requests.post(url, data=json.dumps(data)).json()
logger.DebugLog(res)
if res['code'] == 0:
return res['session']
return None
def bindSession(self, session, qq):
"""校验并激活Session,同时将Session与一个已登录的Bot绑定"""
data = "sessionKey": session, "qq": qq
url = self.addr + 'bind'
res = requests.post(url, data=json.dumps(data)).json()
logger.DebugLog(res)
if res['code'] == 0:
self.session = session
return True
return False
def releaseSession(self, session, qq):
"""不使用的Session应当被释放,长时间(30分钟)未使用的Session将自动释放,
否则Session持续保存Bot收到的消息,将会导致内存泄露(开启websocket后将不会自动释放)"""
data = "sessionKey": session, "qq": qq
url = self.addr + 'release'
res = requests.post(url, data=json.dumps(data)).json()
logger.DebugLog(res)
if res['code'] == 0:
return True
return False
def fetchLatestMessage(self, session):
url = self.addr + 'fetchLatestMessage?count=10&sessionKey='+session
res = requests.get(url).json()
if res['code'] == 0:
return res['data']
return None
def parseGroupMsg(self, data):
res = []
if data is None:
return res
for item in data:
if item['type'] == 'GroupMessage':
type = item['messageChain'][-1]['type']
if type == 'Image':
text = item['messageChain'][-1]['url']
elif type == 'Plain':
text = item['messageChain'][-1]['text']
elif type == 'Face':
text = item['messageChain'][-1]['faceId']
else:
logger.TraceLog(">> 当前消息类型暂不支持转发:=> "+type)
continue
name = item['sender']['memberName']
group_id = str(item['sender']['group']['id'])
group_name = item['sender']['group']['name']
res.append('text': text, 'type': type, 'name': name, 'groupId': group_id, 'groupName': group_name)
return res
def getMessageCount(self, session):
url = self.addr + 'countMessage?sessionKey='+session
res = requests.get(url).json()
if res['code'] == 0:
return res['data']
return 0
def sendPlainTextToGroup(self, session, group, msg):
msg_list = msg.split(r'\\n')
msg_chain = [ "type": "Plain", "text": m+'\\n' for m in msg_list]
data =
"sessionKey": session,
"group": group,
"messageChain": msg_chain
url = self.addr + 'sendGroupMessage'
try:
res = requests.post(url, data=json.dumps(data)).json()
except:
logger.DebugLog(">> 转发失败")
return 0
logger.DebugLog(">> 请求返回:" + str(res))
if res['code'] == 0:
return res['messageId']
return 0
def sendMsgToGroup(self, session, group, msg):
text = msg['text']
type = msg['type']
name = msg['name']
group_id = msg['groupId']
group_name = msg['groupName']
content1 = "【消息中转助手】\\n用户:\\n群号:\\n群名:\\n消息:\\n".format(
name, group_id, group_name, text)
content2 = "【消息中转助手】\\n用户:\\n群号:\\n群名:\\n消息:\\n".format(
name, group_id, group_name)
logger.DebugLog(">> 消息类型:" + type)
if type == 'Plain':
message = ["type": type, "text": content1]
elif type == 'Image':
message = [
"type": 'Plain', "text": content2,
"type": type, "url": text]
elif type == 'Face':
message = ["type": 'Plain', "text": content2,
"type": type, "faceId": text]
else:
logger.TraceLog(">> 当前消息类型暂不支持转发:=> "+type)
return 0
data =
"sessionKey": session,
"group": group,
"messageChain": message
logger.DebugLog(">> 消息内容:" + str(data))
url = self.addr + 'sendGroupMessage'
try:
res = requests.post(url, data=json.dumps(data)).json()
except:
logger.DebugLog(">> 转发失败")
return 0
logger.DebugLog(">> 请求返回:" + str(res))
if res['code'] == 0:
return res['messageId']
return 0
def sendMsgToAllGroups(self, session, receive_groups, send_groups, msg_data):
# 对每条消息进行检查
for msg in msg_data:
group_id = msg['groupId']
# 接收的消息群正确(目前只支持 消息类型)
if group_id in receive_groups:
# 依次将消息转发到目标群
for g in send_groups:
logger.DebugLog(">> 当前群:"+g)
if g == group_id:
logger.DebugLog(">> 跳过此群")
continue
res = self.sendMsgToGroup(session, g, msg)
if res != 0:
logger.TraceLog(">> 转发成功!".format(g))
def sendFriendMessage(self, session, qq, msg):
msg_list = msg.split(r'\\n')
msg_chain = [ "type": "Plain", "text": m+'\\n' for m in msg_list]
data =
"sessionKey": session,
"target": qq,
"messageChain": msg_chain
url = self.addr + 'sendFriendMessage'
try:
res = requests.post(url, data=json.dumps(data)).json()
except:
logger.DebugLog(">> 发送失败")
return 0
if res['code'] == 0:
return res['messageId']
return 0
def qqTransfer():
with open('conf.json', 'r+', encoding="utf-8") as f:
content = f.read()
conf = json.loads(content)
auth_key = conf['auth_key']
bind_qq = conf['bind_qq']
sleep_time = conf['sleep_time']
debug_level = conf['debug_level']
receive_groups = conf['receive_groups']
send_groups = conf['send_groups']
logger.setDebugLevel(debug_level)
session = bot.verifySession(auth_key)
logger.DebugLog(">> session: "+session)
bot.bindSession(session, bind_qq)
while True:
cnt = bot.getMessageCount(session)
if cnt:
logger.DebugLog('>> 有消息了 => '.format(cnt))
logger.DebugLog('获取消息内容')
data = bot.fetchLatestMessage(session)
if len(data) == 0:
logger.DebugLog('消息为空')
continue
logger.DebugLog(data)
logger.DebugLog('解析消息内容')
data = bot.parseGroupMsg(data)
logger.DebugLog(data)
logger.DebugLog('转发消息内容')
bot.sendMsgToAllGroups(session, receive_groups, send_groups, data)
# else:
# logger.DebugLog('空闲')
sleep(sleep_time)
bot.releaseSession(session, bind_qq)
其中,conf.json内容为:
"auth_key": "1234567890",
"bind_qq": "123456", # mirai登录的QQ (复制时记得删我)
"sleep_time": 1,
"receive_groups": ["913182235", "977307922"], # 要接受消息的群 (复制时记得删我)
"send_groups": ["913182235", "977307922"], # 要发送消息的群 (复制时记得删我)
"debug_level": "debug"
下面,我们就先从类似QMsg酱的消息通知开始。
类似QMsg酱的消息通知
设计目标:通过调用指定的URL,小锋仔机器人就会给指定的好友发送指定的消息。
关于QMsg酱的使用教程可以看:免费的QQ微信消息推送机器人
前面我们特地开放了9966端口,因此可以使用Flask来监听这个端口。
本着越简单越好的原则,我们把“发给好友还是群”、“目标好友或群的号”、“发送的内容”三部分都拼接到URL上,因此有:
http://43.143.12.250:9966/QQ/send/friend?target=123&msg=hello
http://43.143.12.250:9966/QQ/send/group?target=123&msg=hello
因此,代码可以写成:
from flask import Flask, request
app = Flask(__name__)
@app.route('/QQ/send/friend', methods=['GET'])
def qqListenMsgToFriend():
# 类似于Qmsg的功能
# flask做得接收HTTP请求转为QQ消息
qq = request.args.get('target', None)
msg = request.args.get('msg', None)
bot.sendFriendMessage(bot.session, qq, msg)
return 'Hello World! Friend!'
@app.route('/QQ/send/group', methods=['GET'])
def qqListenMsgToGroup():
# 类似于Qmsg的功能
# flask做得接收HTTP请求转为QQ消息
qq = request.args.get('target', None)
msg = request.args.get('msg', None)
bot.sendPlainTextToGroup(bot.session, qq, msg)
return 'Hello World! Group!'
if __name__ == '__main__':
app.run(port='9966', host='0.0.0.0')
由于Flask和小锋仔QQBot都要阻塞运行,因此稍微变动一下,让小锋仔以子线程的形式运行即可。
if __name__ == '__main__':
t = threading.Thread(target=qqTransfer)
t.setDaemon(True)
t.start()
app.run(port='9966', host='0.0.0.0')
测试一下:
http://localhost:9966/QQ/send/friend?target=1061700625&msg=hello
如果我们把这个脚本放到服务器上去运行,那么链接就变成了:
http://43.143.12.250:9966/QQ/send/friend?target=1061700625&msg=hello
当然,能发消息的前提是“先加好友”或“加群”啦。
多功能切换的实现设计
上面我们进行了简单地尝鲜。
1、从这部分开始,我们涉及的功能比较杂,为了能更好的区分功能,需要设计一个简单的交互协议。
- 我们发送的内容可以分为:功能选择 与 消息详情;
- 为了区分他俩,可以在选择功能时添加指定前缀,如“CMD+翻译”;
- 小锋仔接收到后,进入翻译模式准备;
- 发送指令详情时,就不加前缀。而小锋仔则将收到的消息进行翻译,再把结果返回。
根据以上内容,小锋仔需要记录的状态信息至少有:
class StatusStore:
def __init__(self, from_qq:int=None, is_cmd:bool=False, func_name:str=None, need_second:bool=False, msg:str=None) -> None:
self.from_qq = from_qq # 发送者的QQ号
self.is_cmd = is_cmd # 是否是指令(选择功能)
self.func_name = func_name # 选择的功能的名称
self.need_second = need_second # 是否需要经过两步:先发cmd指令,再发详细内容
self.msg = msg # 本次发送的消息内容
def detail(self):
return self.__dict__
2、并且我们设置,只有从指定QQ发过来消息,才能响应。因此在接收到消息时,需要判断对方的信息。对于好友类型的消息,mirai返回格式如消息类型说明:
"type": "FriendMessage",
"sender":
"id": 123,
"nickname": "",
"remark": ""
,
"messageChain": [] // 数组,内容为下文消息类型
因此,我们可以从"type"和 "sender:id"入手判断。
3、我们暂时考虑只有一个主QQ能发送指令的情况。
4、定义一个类来专门管理不同功能的函数,例如:
class MultiFunction:
"""多功能函数集合"""
def __init__(self) -> None:
pass
@staticmethod
def translate(original:str, convert:str='zh2en') -> str:
return '假装是翻译结果'
@staticmethod
def uploadImage(image_path:str) -> str:
return '假装是上传结果'
@staticmethod
def weather(city:str) -> str:
return '假装是天气结果'
@staticmethod
def hotNews(status_store:StatusStore) -> str:
return '假装是热搜结果'
# 多功能函数的映射
# function: 功能对应函数名
# need_second: 是否需要经过两步:先发cmd指令,再发详细内容
# desc: 需要经过两步时,第一次返回的提示语
function_map =
'翻译': 'function': MultiFunction.translate, 'need_second': True, 'desc': '请输入您要翻译的内容~',
'天气': 'function': MultiFunction.weather, 'need_second': True, 'desc': '请问是哪座城市的天气呢?',
'热搜': 'function': MultiFunction.hotNews, 'need_second': False
def choiceFunction(store_obj:StatusStore):
res = ''
if function_map.get(store_obj.func_name):
res = function_map.get(store_obj.func_name)['function'](store_obj.msg)
return res
5、大致实现流程的想法是:
对应代码实现:
def analyzeFriendMsg(self, data):
if data is None or data['type'] != 'FriendMessage':
return None, None, None
sender_id = data['sender']['id']
msg_type = data['messageChain'][-1]['type']
if msg_type == 'Plain':
msg_text = data['messageChain'][-1]['text']
elif msg_type == 'Image':
msg_text = data['messageChain'][-1]['url']
else:
msg_text = ''
return sender_id, msg_type, msg_text
最终的框架就是:
def xiaofengzai():
auth_key = '1234567890' # settings.yml中的verifyKey
bind_qq = '3126229950' # mirai登录的QQ
target_qq = '1061700625' # 我们自己用的主QQ
target_qq = int(target_qq) # 接收到的消息里,QQ是int类型的
sleep_time = 1 # 轮询间隔
status_store =
session = bot.verifySession(auth_key)
logger.DebugLog(">> session: "+session)
bot.bindSession(session, bind_qq)
while True:
cnt = bot.getMessageCount(session)
if not cnt:
sleep(sleep_time)
continue
logger.DebugLog('>> 有消息了 => '.format(cnt))
logger.DebugLog('获取消息内容')
data = bot.fetchLatestMessage(session)
if len(data) == 0:
logger.DebugLog('消息为空')
sleep(sleep_time)
continue
logger.DebugLog(data)
logger.DebugLog('解析消息内容')
sender_id, msg_type, msg_text = bot.analyzeFriendMsg(data[0])
if not sender_id or sender_id != target_qq:
sleep(sleep_time)
continue
if msg_text.strip().lower().startswith('cmd'):
_, func_name = msg_text.strip().split('\\n')[0].split()
func_name = func_name.strip()
store_obj = StatusStore(from_qq=sender_id, is_cmd=True, func_name=func_name)
# 不需要发两次,直接调用函数返回结果即可
func_info = function_map.get(func_name)
if not func_info:
res = '指令[]暂不支持'.format(func_name)
elif func_info.get('need_second'):
res = '收到你的指令:\\n'.format(func_name, func_info.get('desc') or '已进入对应状态, 请继续发送详细内容')
# 添加或更新记录
status_store[sender_id] = store_obj
else:
res = '请求结果为:\\n' + str(choiceFunction(store_obj))
status_store.pop(sender_id, '')
else:
res = '请先发送指令哦...'
store_obj = status_store.get(sender_id)
if store_obj and store_obj.is_cmd:
store_obj.msg = msg_text
res = '请求结果为:\\n' + str(choiceFunction(store_obj))
status_store.pop(sender_id, '')
bot.sendFriendMessage(session, qq=sender_id, msg=res)
看一下效果:
至此,骨架有了,接下来开始填充功能了。
翻译查询
根据上面的骨架可知,我们只需要实现MultiFunction类下的translate函数即可。如果想快速测试函数效果,可以使用以下代码,而不用先启动mirai:
res = choiceFunction(StatusStore(func_name='翻译', msg='你好'))
print(res)
领取腾讯免费翻译API
要做翻译,最方便的就是调用API了(没错,调包侠!)。
这里使用腾讯的翻译API,可以免费领取:领取腾讯翻译API。点进链接后,往下拖到“云产品体验”专区,选择“人工智能”,下面有“机器翻译”。他的调用量是每月更新,非常的良心了。
点击“立即体验”,进入控制台界面,虽然上面显示的是“开通付费版”,但不用担心,他是有免费额度的,更何况你账户里又没充余额,哈哈哈。
支持很多类型的翻译,这次我们先选文本翻译,机器翻译 文本翻译-API 文档-文档中心-腾讯云:
我们用SDK的方式,免去了自己封装复杂的加密步骤:
pip install --upgrade tencentcloud-sdk-python
然后去获取密钥API密钥管理,记下APPID、SecretId、SecretKey:
机器人接入翻译功能
小锋仔bot结合翻译功能,直接上代码:
import json
from tencentcloud.common import credential
from tencentcloud.common.profile.client_profile import ClientProfile
from tencentcloud.common.profile.http_profile import HttpProfile
from tencentcloud.common.exception.tencent_cloud_sdk_exception import TencentCloudSDKException
from tencentcloud.tmt.v20180321 import tmt_client, models
def translate(original:str, convert:str='en'):
secretId = 'xxx' # 从API控制台获取
secretKey = 'xxx' # 从API控制台获取
AppId = 12123 # 从API控制台获取
try:
cred = credential.Credential(secretId, secretKey)
client = tmt_client.TmtClient(cred, "ap-guangzhou")
req = models.TextTranslateRequest()
params =
"SourceText": original,
"Source": "auto",
"Target": convert,
"ProjectId": AppId
req.from_json_string(json.dumps(params))
resp = client.TextTranslate(req)
# print(resp.to_json_string())
return resp.TargetText
except TencentCloudSDKException as err:
print(err)
return ''
使用测试效果:
print(choiceFunction(StatusStore(func_name='翻译', msg='你好')))
# 输出:
"TargetText": "Hello", "Source": "zh", "Target": "en", "RequestId": "a1b17f47-751e-44cd-89a5-6a22e9f2c444"
Hello
实时天气
领取免费的和风天气API
天气部分,我们是用免费的和风天气API:实时天气 - API。
首先也要进行登录并获取KEY,这个步骤官网讲的很详细,图文并茂的,这边就不多写了,大家可以跳转过去(注意我们选的是Web API):创建应用和KEY - RESOURCE。
机器人接入天气功能
同样的,直接上代码:
def weather(city:str) -> str:
url_api_weather = 'https://devapi.qweather.com/v7/weather/now?'
url_api_geo = 'https://geoapi.qweather.com/v2/city/lookup?'
weather_key = 'xxxxx' # 和风天气控制台的key
# 实况天气
def getCityId(city_kw):
url_v2 = url_api_geo + 'location=' + city_kw + '&key=' + weather_key
city = requests.get(url_v2).json()['location'][0]
return city['id']
city_name = '广州'
city_id = getCityId(city_name)
url = url_api_weather + 'location=' + city_id + '&key=' + weather_key
res = requests.get(url).json()
text = "<天气信息获取失败>"
if res['code'] == '200' or res['code'] == 200:
text = '实时天气:\\n 亲爱的 小主, 您所在的地区为 ,\\n 现在的天气是 ,\\n 气温 °, 湿度 %,\\n 体感气温为 °,\\n 风向 , 风速 km/h'.format(
city_name, res['now']['text'], res['now']['temp'], res['now']['humidity'], res['now']['feelsLike'], res['now']['windDir'], res['now']['windSpeed'])
return text
测试效果:
1.1 函数(Function)是什么? 函数可以看做是功能(以一辆汽车为例,如下图),这些都可以看做成是方法 1.2 一个最简单的函数及触发方法 1.3 带参数的函数(形参与实参) 1.4 带有返回值的函数 ———— return 1.5 js函数内置对象 ———— arguments(重点,考点) 1.6 函数内的变量 1.7 匿名函数(难点) 匿名自执行函数(类似于JS的单例模式) 2.1 HTML事件 常用事件整理 2.2 JavaScript 事件一般用于做什么? 2.3 事件实例 在JS里 —— 万物皆为对象 3.1 对象定义 3.2 大厂经典面试题分析 let obj = Object.create(null) 与 let obj = {} 有什么区别? 所以这种区别导致了使用场景不同 执行结果 4.1 prototype 继承 所有的 JavaScript 对象都会从一个 prototype(原型对象)中继承属性和方法: 所有 JavaScript 中的对象都是位于原型链顶端的 Object 的实例 4.2 添加属性和方法 5.1 从this说起 如何解决this指向问题? 使用ES6中箭头函数 函数内部使用_this = this 使用apply,call,bind方法 new实例化一个对象 5.2 谈谈apply,call,bind apply() 方法调用一个函数,其具有一个指定的this值,以及作为一个数组(或者类似数组的对象)提供的参数,fun.apply(thisArg, [argsArray]) thisArg:在fun函数运行时指定的this值。指定this的值并不一定是函数执行时真正的this值,如果是原始值的this会指向该原始值的自动包装对象。 argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给fun函数。参数为null或者undefined,则表示不需要传入任何参数。 call() 调用一个函数,其具有一个指定的this值,以及若干个参数列表,fun.call(thisArg, arg1, arg2, …) thisArg:在fun函数运行时指定的this值。指定this的值并不一定是函数执行时真正的this值,如果是原始值的this会指向该原始值的自动包装对象。 arg1, arg2, …:若干个参数列表 bind() 创建一个新的函数,当被调用时,将其this的关键字设置为提供的值,在调用新函数时,在任何提供一个给定的参数序列。 bind创建了一个新函数,必须手动去调用。 5.3 区别 事件冒泡:事件开始由最具体的元素接受,然后逐级向上传播 事件捕捉:事件由最不具体的节点先接收,然后逐级向下,一直到最具体的(与上面相反) DOM事件流:三个阶段:事件捕捉,目标阶段,事件冒泡 7.1 函数防抖 应用场景(数据抖动问题) 7.2 函数节流 应用场景(客运站问题) 把整个事件处理器比喻成客运站,如果客运大巴到站就走,那么路上肯定会发生交通拥堵,而且车大部分是空的 因为没给时间上客,虚假繁忙的情况肯定是不好的,那么怎么处理呢? 设置一个时间间隔,时间间隔内只允许执行一次,客运站大巴设定一个时间,到点才会走 8.1 为什么要有虚拟dom? 8.2 虚拟dom是什么?好处是? 1. 希望本文能对大家有所帮助,如有错误,敬请指出 2. 原创不易,还请各位客官动动发财的小手支持一波(关注、评论、点赞、收藏)JavaScript保姆级教程 ——— 重难点详细解析(建议收藏)
1. JS函数
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>我的第一个方法</title>
</head>
<body>
<button onclick="myFunction()">点击触发函数</button>
<script>
// 必须有 function关键字,命名通常为驼峰命名,首字母小写
function myFunction(){
alert("这是我的函数");
}
</script>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>形参与实参</title>
</head>
<body>
<!-- 这里的5和2是实参 -->
<button onclick="addNum(5, 2)">计算5+2的值</button>
<script>
// 此处的num1,与num2便是形参
function addNum(num1, num2){
alert(num1 + num2)
}
</script>
</body>
</html>
function fn(a, b){
return a*b;
}
// 调用并给num赋值
let num = fn(3, 5);
console.log(num) // 得到15
function fn(){
console.log(arguments)
}
fn(1, 2, 3, 4);
经典应用 ———— 求一组参数的总和 function fn(){
let sum = 0;
for(let i = 0; i < arguments.length; i++){
sum += arguments[i];
}
// 返回 sum
return sum
}
let allSum = fn(1, 2, 3, 4);
console.log(allSum) // 得到10
function fn() {
// 此为局部变量
let a = 5;
console.log(a)
}
fn();
console.log(a) // 此处报错,无法访问
(function (){
//由于没有执行该匿名函数,所以不会执行匿名函数体内的语句。
console.log("666");
})
(function (){
console.log("666"); // 此处会打印666
})()
2. JS事件
事件名 说明 onchange() HTML 元素改变(一般用于表单元素) onclick () 用户点击 HTML 元素 onmouseover() 用户在一个HTML元素上移动鼠标 onmouseout() 用户从一个HTML元素上移开鼠标 onkeydown() 用户按下键盘按键 onkeyup() 键盘按键弹起 onload() 浏览器已完成页面的加载
<input id="test" type="button" value="提交"/>
<script>
// 页面加载完触发
window.onload = function(){
let test = document.getElementById("test");
test.addEventListener("click",myfun2);
test.addEventListener("click",myfun1);
}
function myfun1(){
alert("你好1");
}
function myfun2(){
alert("你好2");
}
</script>
3. JavaScript 对象
// 对象定义
let person = {
firstName:"ouyang",
lastName:"xiu",
age:18
};
// 循环对象
for(let key in person){
console.log(key); // 键名
console.log(person[key]) // 键值
}
let obj = {};
let obj2 = Object.create(null);
console.log(obj);
console.log(obj2)
4. JavaScript prototype(原型对象)
// 创建构造函数
function Person(name, age) {
this.age = age;
this.name= name;
this.fn = function(){
console.log(this.name)
}
}
// 创建实例
let person1 = new Person("小明", 18);
let person2 = new Person("小红", 20);
person1.fn(); // 继承父级的方法
person2.fn();
console.log(person1)
console.log(person2)
function Person(name, age, sex) {
// sex为新属性
this.sex = sex;
this.age = age;
this.name= name;
this.fn = function(){
console.log(this.name)
}
}
function Person(name, age, sex) {
// sex为新属性
this.sex = sex;
this.age = age;
this.name= name;
this.fn = function(){
console.log(this.name)
}
}
Person.prototype.newVal = "我是新添加在原型上的值";
let person1 = new Person("小明", 18);
console.log(person1)
5. call和apply及bind三者的区别(面试重点)
let obj = {
name : "小明",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.apply(name),1000);
}
};
obj.func2() // 小明
let obj2 = {
name : "小红",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.call(name),1000);
}
};
obj2.func2() // 小红
let obj3 = {
name : "小猪",
func1: function () {
console.log(this.name)
},
func2: function () {
setTimeout( function () {
this.func1()
}.bind(name)(),1000);
}
};
obj3.func2() // 小猪
6. Javascript的事件流模型(面试重点)
7. 防抖与节流(面试精选)
let telInput = document.querySelector('input');
telInput.addEventListener('input', function(e) {
//如果直接每次发请求,会导致性能问题
//数据请求
let timeOut = null;
if(timeOut){
clearTimeout(timeOut)
}else{
timeOut = setTimeout(()=>{
$.ajax({})
},2000)
}
})
let throttle = function(func, delay) {
let prev = Date.now();
return function() {
var context = this;
var args = arguments;
var now = Date.now();
if (now - prev >= delay) {
func.apply(context, args);
prev = Date.now();
}
}
}
function demo() {
//do something
//ajax({})
//...
}
box.addEventListener('touchmove', throttle(demo, 2000));
8. JS中的虚拟DOM是什么?(面试重点)
9. 手写一个new,实现同等功能
function Person(name) {
this.name = name
this.sayName= function () {
console.log(`我是 ${this.name}!`)
}
}
function myNew(that, ...args) {
const obj = Object.create(null)
obj.__proto__ = that.prototype
const res = that.call(obj, ...args)
return res instanceof Object ? res : obj
}
let person= myNew(Person, '小明')
person.sayWorld(); // 我是小明
10. 获得页面url参数的值(常用)
function getQueryString(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
3. 拜谢各位!