Python + Go-cqhttp + Pyqt5 —— 搭建QQ机器人

Go-cqhttp

简介

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)

这样看着就舒服很多了在这里插入图片描述

测试API文档

在这里插入图片描述
api文档非常的全面,请求的路径参数及返回都很清楚
在这里插入图片描述

简单测试一个发送消息:

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"}

在这里插入图片描述
优化api,方便以后调用

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也是有文档说明的,很容易上手

Pyqt5制作交互GUI

创建窗口

使用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日志读取

因为使用的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帮助中心有写

UI登录

在输入账号密码后点击登录触发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定义的pyqtSignaldict
如: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)

初级UI展示

在这里插入图片描述

yml配置文件读写

现在我们还是根据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了

发消息UI

在这里插入图片描述
用三个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)

完整页面

其他的页面也都是差不多的,根据api来设计ui
在这里插入图片描述

群聊管理

文字识别自动操作

群管理(首先登录的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)

结束

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值