go-cqhttp是使用 mirai 以及 MiraiGo 开发的 cqhttp golang 原生实现,并在 cqhttp 原版 的基础上做了部分修改和拓展。
而cqhttp是 酷Q机器人,酷Q机器人大多数人应该接触过。
HTTP API
反向HTTP POST
正向WebSocket
反向WebSocket
HTTP POST 多点上报
反向 WS 多点连接
修改群名
消息撤回事件
解析/发送 回复消息
解析/发送 合并转发
使用代理请求网络图片
在关闭数据库的情况下, 加载 25 个好友 128 个群运行 24 小时后内存使用为 10MB 左右. 开启数据库后内存使用将根据消息量增加 10-20MB , 如果系统内存小于 128M 建议关闭数据库使用。
GitHub地址 go-cqhttp
应用下载地址
go-cqhttp 帮助中心
我是在win10上面搭建,所以我这里选择go-cqhttp_windows_amd64.zip,下载好解压缩后会看到三个文件主程序就是go-cqhttp.exe
1.双击运行go-cqhttp.exe, 一路确认之后文件夹会多出一个bat的文件
2.双击go-cqhttp.bat
我这里是要是要api进行交互,所以选择http通信,输入0回车,生成config.yml配置文件
3.修改config.yml配置文件
这里需要修改qq和密码,如果不设置密码则使用二维码的登录方式,二维码登录需要地区相同,不然会登录不上,如果服务器部署则可以使用代理讲手机的网络换到与服务器相同的网络地址
由于我们使用的是http,需要修改配置文件的server配置,将url注释打开
登录完成后,会生成data数据文件夹,logs日志文件夹,device.json的设备信息,session.token身份信息
使用命令行参数 faststart即可跳过启动的五秒钟延时,例如
# Windows
.\go-cqhttp.exe -faststart
# Linux
./go-cqhttp -faststart
当我们登录成功后所有的消息都在cmd里显示出了
接收消息,使用socket链接,循环接收,recv(1024) 是接收最大字节,后续使用中 字数太多会接收不到
host = '127.0.0.1'
port = 5701
HttpResponseHeader = '''HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n\r\n
'''
ListenSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ListenSocket.bind((host, port))
ListenSocket.listen(100)
while True:
Client, Address = ListenSocket.accept()
Request = Client.recv(1024).decode(encoding='utf-8')
print(Request)
Client.sendall((HttpResponseHeader).encode(encoding='utf-8'))
Client.close()
启动go-cqhttp在启动代码后能接收到如下消息,就说明已经成功了,这是心跳包
由于接受到的消息含有headers的消息,而我们只需要返回中的data信息,提取data,优化后的代码
host = '127.0.0.1'
port = 5701
HttpResponseHeader = '''HTTP/1.1 200 OK\r\n
Content-Type: text/html\r\n\r\n
'''
def start_server():
ListenSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
ListenSocket.bind((host, port))
ListenSocket.listen(100)
return ListenSocket
def request_to_json(msg):
return json.loads(msg[msg.index('{'):])
def rev_msg(ListenSocket): # json or None
Client, Address = ListenSocket.accept()
Request = Client.recv(1024).decode(encoding='utf-8')
rev_json = request_to_json(Request)
Client.sendall((HttpResponseHeader).encode(encoding='utf-8'))
Client.close()
return rev_json
ListenSocket = start_server()
while True:
result = rev_msg(ListenSocket)
print(result)
简单测试一个发送消息:
url = 'http://127.0.0.1:5700/send_private_msg?user_id=1160606738&message=你好啊!我是go-cqhttp'
response = requests.get(url=url)
print(response.text)
# response.text
{"data":{"message_id":-242786616},"retcode":0,"status":"ok"}
host = 'http://127.0.0.1:5700'
send_private_msg_path = '/send_private_msg?' # 发送私聊
def request_url(url):
response = requests.get(url=url, verify=False)
return response.json()
def send_private_msg(message, user_id):
"""
发送私聊
:param message: 内容
:param user_id: QQ号
:return: message_id
文档:::
请求参数
user_id int64 - 对方 QQ 号
group_id int64 - 主动发起临时会话时的来源群号(可选, 机器人本身必须是管理员/群主)
message message - 要发送的内容
auto_escape boolean false 消息内容是否作为纯文本发送 ( 即不解析 CQ 码 ) , 只在 message 字段是字符串时有效
请求返回
message_id int32 消息 ID
"""
send_message = {'user_id': user_id, 'message': message}
url = host + send_private_msg_path + urlencode(send_message)
result = request_url(url)
return result
总体搭建还是很简单的,需要修改的东西不多,所有的api也是有文档说明的,很容易上手
使用QGroupBox容器组件会更方便的规划区域,页面也会看起来更整洁
# UI
class QQ_ROBOT(QMainWindow):
def __init__(self):
super().__init__()
self.initUI()
self.show()
def set_text_size(self, size, bold):
"""
设置字体 是否加粗
:param size:
:param bold:
:return:
"""
font = QtGui.QFont()
font.setFamily("宋体")
font.setPointSize(size * 90 / 72)
font.setBold(bold)
return font
def only_int(self):
reg = QRegExp('[0-9]+$')
validator = QRegExpValidator(self)
validator.setRegExp(reg)
return validator
def windows_info(self):
self.setFixedSize(940, 500) # 窗体尺寸
self.setWindowTitle('QQ机器人') # 标题
self.setAcceptDrops(True)
def initUI(self):
self.read_settings()
self.windows_info() # 窗口本体设置
self.windows_box() # 登录信息
self.log_browser() # 日志窗口
self.login_box_info() # 登录信息
def read_settings(self):
self.settings = QSettings("QQROBOT.ini", QSettings.IniFormat)
def windows_box(self):
# 登录信息
self.login_box = QGroupBox('登录信息', self)
self.login_box.setGeometry(QRect(720, 340, 210, 140))
self.login_box.setFont(self.set_text_size(8, False))
self.login_box.setStyleSheet("QGroupBox {border: 1px solid #4e6ef2;}")
def login_box_info(self):
account_l = QLabel('账号: ', self.login_box)
account_l.setGeometry(QRect(10, 20, 40, 20))
account_l.setFont(self.set_text_size(9, False))
password_l = QLabel('密码: ', self.login_box)
password_l.setGeometry(QRect(10, 60, 40, 30))
password_l.setFont(self.set_text_size(9, False))
self.account_le = QLineEdit(self.login_box)
self.account_le.setGeometry(QRect(account_l.x() + account_l.width(), account_l.y() - 2, 150, 25))
self.account_le.setFont(self.set_text_size(8, False))
self.account_le.setText(self.settings.value('QQ')) if self.settings.value('QQ') else ''
self.account_le.setValidator(self.only_int())
self.password_le = QLineEdit(self.login_box)
self.password_le.setGeometry(QRect(password_l.x() + password_l.width(), password_l.y() - 2, 150, 25))
self.password_le.setFont(self.set_text_size(8, False))
self.password_le.setText(self.settings.value('PW')) if self.settings.value('PW') else ''
self.password_le.setEchoMode(QLineEdit.Password)
self.login_b = QPushButton("登录", self.login_box)
self.login_b.setGeometry(QRect(10, 100, 80, 30))
self.login_b.setFont(self.set_text_size(10, False))
self.login_b.clicked.connect(self.login)
self.l_out_b = QPushButton("注销", self.login_box)
self.l_out_b.setGeometry(QRect(120, 100, 80, 30))
self.l_out_b.setFont(self.set_text_size(10, False))
self.l_out_b.clicked.connect(self.loginout)
self.l_out_b.setEnabled(False)
def log_browser(self):
# 日志 显示框
self.log_b = QTextBrowser(self)
self.log_b.setGeometry(QRect(10, 340, 700, 140))
self.log_b.setStyleSheet("background-color: black;color: green")
def login(self):
pass
def loginout(self):
pass
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = QQ_ROBOT()
sys.exit(app.exec_())
注:
only_int(self): 输入框只能输入数值而不能输入其他字符,以免输入错误,降低程序的出错率
set_text_size(self, size, bold): 设置字体大小及是否加粗显示
setEchoMode(QLineEdit.Password) 输入框密码输入形式, 不显示真正输入
因为使用的cqhttp是exe,需要在Python程序中启动,还需要在cqhttp启动的时候读取cmd的日志信息然后再gui中的日志区显示,在网上找了一段代码,用于读取cmd日志的
def run_cmd(_cmd):
"""
开启子进程,执行对应指令,控制台打印执行过程,然后返回子进程执行的状态码和执行返回的数据
:param _cmd: 子进程命令
:return: 子进程状态码和执行结果
"""
p = subprocess.Popen(_cmd, shell=True, close_fds=False, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, encoding='utf8')
_RunCmdStdout, _ColorStdout = [], '\033[1;35m{0}\033[0m'
while p.poll() is None:
try:
line = p.stdout.readline().rstrip().replace('\x1b', '').replace('[0m', '').replace('[33m', '').replace('[37m', '').replace('\n', '')
except:
line = ''
if not line:
continue
print(line)
_RunCmdStdout.append(line)
print(_ColorStdout.format(line))
last_line = p.stdout.read().rstrip().replace('\x1b', '').replace('[0m', '').replace('[33m', '').replace('[37m', '').replace('\n', '')
if last_line:
_RunCmdStdout.append(last_line)
print(_ColorStdout.format(last_line))
_RunCmdReturn = p.wait()
return _RunCmdReturn, '\n'.join(_RunCmdStdout), p.stderr.read(), p
run_cmd('go-cqhttp.exe -faststart')
# -faststart 是取消cqhttp在启动的等待时间(5s), 在go-cqhttp帮助中心有写
在输入账号密码后点击登录触发login方法,再启动登录的线程(QThread
)。
登录前需要验证是否输入了账号密码,反之提示账号密码不能为空
验证输入之后去改变ini设置文件记录的qq和密码,这样在ui中去读取设置文件就不用每次都输入了
def login(self):
self.account_text = self.account_le.text()
self.password_text = self.password_le.text()
if self.account_text and self.account_text:
self.settings.setValue('QQ', self.account_text)
self.settings.setValue('PW', self.password_text)
self.R_t = QQThread(self) # 创建线程
self.R_t._signal.connect(self.log_info) # 连接信号
self.R_t.start()
self.R_t.exec()
self.login_b.setEnabled(False)
else:
QMessageBox.warning(self, "错误", '账号密码不能为空')
在线程中首先将5701的监听启动,再启动go-cqhttp, 线程go_http_执行的就是run_cmd('go-cqhttp.exe -faststart')
run_cmd
读取到的日志通过pyqtSignal信号传入ui进行展示,在QThread
定义的pyqtSignal
为dict
如:print(_ColorStdout.format(line))
修改为T_self._signal.emit({'message_type': 'log', 'message': f'{line}'})
这样传递数据,在ui中也可以根据message_type区别数据,更好处理
class QQThread(QThread):
"""
消息线程
"""
_signal = pyqtSignal(dict)
def __init__(self, Q_self):
self.Q_self = Q_self
super(QQThread, self).__init__()
def run(self):
# 启动5701监听
threading.Thread(target=receive_, args=(self,), daemon=True).start()
# 启动go-http
threading.Thread(target=go_http_, args=(self,), daemon=True).start()
处理好之后启动ui,输入账号密码,点击登录,即可看到日志在ui中输出了
在登录和注销的操作中,我的处理方式是未登录:注销按钮无法点击,登录后登录按钮无法点击,反之则释放按钮setEnabled(True)
现在我们还是根据yml配置文件的qq登录的,在ui中输入的账户密码是不能用的,所以还需要将ui输入的账号密码在yml的配置文件中生效,在登录之前需要修改yml文件
# 读写yml文件
import yaml
def read_yml(file):
"""读取yml,传入文件路径file"""
f = open(file, 'r', encoding="utf-8") # 读取文件
yml_config = yaml.load(f, Loader=yaml.FullLoader) # Loader为了更加安全
return yml_config
def write_yml(file, data):
# 写入数据:
with open(file, "w", encoding='utf-8') as f:
# data数据中有汉字时,加上:encoding='utf-8',allow_unicode=True
yaml.dump(data, f, encoding='utf-8', allow_unicode=True)
在QQThread run中增加修改yml账号密码
首先需要判断的是否有yml文件 -> 账号是否相同 -> 若不同修改账号密码
注: 不同账号登录需要把其他文件删除,不然会登录错误,索性将其他文件全部删除只保留未登录时的文件
is_1 = os.path.exists('go-cqhttp/config.yml')
if is_1:
account_data = read_yml('go-cqhttp/config.yml')
if account_data.get('account').get('uin') != self.account:
# 删除缓存文件
files = ['data', 'logs', 'device.json', 'session.token']
for file in files:
file_path = 'go-cqhttp/' + file
is_file = os.path.exists(file_path)
if is_file:
if os.path.isdir(file_path):
shutil.rmtree(file_path) # 删除文件夹
if os.path.isfile(file_path):
os.remove(file_path) # 删除文件
# 不一样 写入QQ
account_data['account']['uin'] = self.account
account_data['account']['password'] = self.password
write_yml('go-cqhttp/config.yml', data=account_data)
这样就可以随意的切换QQ了
用三个QRadioButton
选择需要发送的对象
当发送好友和群聊的时候还可以输入需要屏蔽的QQ号和群聊号
def message_box_info(self):
self.message_1 = QRadioButton('个人', self.message_box)
self.message_1.setGeometry(QRect(20, 10, 50, 30))
self.message_1.setFont(self.set_text_size(8, False))
self.message_1.clicked.connect(self.click_sender)
self.message_1.setChecked(True)
self.message_2 = QRadioButton('好友', self.message_box)
self.message_2.setGeometry(QRect(80, 10, 50, 30))
self.message_2.setFont(self.set_text_size(8, False))
self.message_2.clicked.connect(self.click_sender)
self.message_3 = QRadioButton('群聊', self.message_box)
self.message_3.setGeometry(QRect(140, 10, 50, 30))
self.message_3.setFont(self.set_text_size(8, False))
self.message_3.clicked.connect(self.click_sender)
one_qq_l = QLabel('个人Q', self.message_box)
one_qq_l.setGeometry(QRect(10, self.message_1.y() + self.message_1.height(), 40, 30))
one_qq_l.setFont(self.set_text_size(8, False))
message_l = QLabel('消息\n设置', self.message_box)
message_l.setGeometry(QRect(10, one_qq_l.y() + one_qq_l.height(), 40, 50))
message_l.setFont(self.set_text_size(9, False))
left_qq_l = QLabel('屏蔽Q', self.message_box)
left_qq_l.setGeometry(QRect(10, message_l.y() + message_l.height() + 10, 40, 30))
left_qq_l.setFont(self.set_text_size(8, False))
self.one_qq_le = QLineEdit(self.message_box)
self.one_qq_le.setGeometry(QRect(one_qq_l.x() + one_qq_l.width(), one_qq_l.y(), 150, 25))
self.one_qq_le.setFont(self.set_text_size(8, False))
self.one_qq_le.setValidator(self.only_int())
self.message_le = QTextEdit(self.message_box)
self.message_le.setGeometry(QRect(message_l.x() + message_l.width(), message_l.y(), 150, 50))
self.message_le.setFont(self.set_text_size(8, False))
self.message_le.setText(self.settings.value('m_1')) if self.settings.value('m_1') else self.message_le.setText('')
self.left_qq_le = QLineEdit(self.message_box)
self.left_qq_le.setGeometry(QRect(left_qq_l.x() + left_qq_l.width(), left_qq_l.y(), 150, 25))
self.left_qq_le.setFont(self.set_text_size(8, False))
self.left_qq_le.setPlaceholderText('多个用逗号(,)隔开')
self.time_c = QComboBox(self.message_box)
self.time_c.setGeometry(QRect(10, self.left_qq_le.y() + self.left_qq_le.height() + 10, 100, 30))
self.time_c.setFont(self.set_text_size(10, False))
self.time_c.addItems(['发送设置', '每分钟', '每小时', '每天'])
self.send_b = QPushButton("发送", self.message_box)
self.send_b.setGeometry(QRect(120, self.left_qq_le.y() + self.left_qq_le.height() + 10, 80, 30))
self.send_b.setFont(self.set_text_size(10, False))
self.send_b.clicked.connect(self.send)
群管理(首先登录的qq得是管理员或者是群主才可以操作):自动禁言、自动撤回和自动踢人功能是根据输入框的关键字和群成员发送的消息进行匹配 当关键字命中消息则执行相关的操作,
如果发送的是图片文字的形式,就需要用到图片 OCR
先识别图片中的文字,再去关键字命中,也可以达到撤回等效果
# 在图片消息中提取图片的id,用api中的ocr识别
image_id = re.findall(r'file=(.*?).image', raw_message)[0]
result = ocr_image(f'{image_id}.image') # 图片ocr
raw_message = ''.join([text.get('text') for text in result.get('data').get('texts')])
需要群主或者是管理员艾特要被操作的人,当接收到艾特消息会先识别发送的身份,是否符合条件
# 群主管理员艾特
qq= re.findall(r'qq=(.*?)]', raw_message)[0]
if '踢' in raw_message:
set_group_kick(group_id, qq, False)
elif '禁' in raw_message:
set_group_ban(group_id, qq, 120)
elif '解' in raw_message:
set_group_ban(group_id, qq, 0)