Nemo2011 / bilibili-api

哔哩哔哩常用API调用。支持视频、番剧、用户、频道、音频等功能。原仓库地址:https://github.com/MoyuScript/bilibili-api
https://nemo2011.github.io/bilibili-api/
GNU General Public License v3.0
2.14k stars 203 forks source link

[提问] 私信消息监听偶尔会重复接收前几次消息 #730

Open xiaoxiaohaoa opened 6 months ago

xiaoxiaohaoa commented 6 months ago

Python 版本: 3.9

模块版本: 16.1.1

运行环境: Linux


我参考示例文档(示例:保存私信收到的图片,并对文字私信做出回复)给出的内容,试着实现了根据收到的私信关键词自动发送对应回复的功能。

以下是部分指令与对应的自动回复内容:

        if event.content == "/终止": #杀死进程
            await session.reply(event, "脚本已停运")
            session.close()

        elif event.content == "/开始": #用于开始监听的口令,触发后将开始自动回复私信
            await session.reply(event, "脚本已开启")
            status.append(token)

        elif event.content == "/结束": #用于结束监听的口令,触发后将不再自动回复私信
            await session.reply(event, "脚本已暂停")
            status.clear()

按照预期,一个关键词仅能触发一次自动回复。 例如,依照上述段落,对方发送“/终止”时,日志内容应该类似:

[...][INFO]: 收到 [uid] 信息 /终止(...)
[...][INFO]: 发送 [uid] 信息 脚本已停运(...)
[...][INFO]: 结束轮询

最初确实是能按照预期运行的。但是,每当运行一段时间后,偶尔会出现对方发送一个关键词,自动回复却连续回复许多不同的内容的情况。 以下是最近一次通过小号发送内容为“/终止”的私信后生成的日志:

[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 脚本已暂停(1710683817)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 /终止(1710729034)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 脚本已停运(1710729038)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 /开始(1710729095)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 脚本已开启(1710729097)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 test1(1710729112)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 response1(1710729115)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 /结束(1710729129)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 脚本已暂停(1710729133)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 /开始(1710729750)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 脚本已开启(1710729755)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 test2(1710729768)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 response2(1710729772)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 /结束(1710729809)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 脚本已暂停(1710729814)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 /终止(1710729841)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 脚本已停运(1710729844)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 test3(1710730001)
[2024-03-26 07:24:02][INFO]: 发送 [uid] 信息 response3(1710730009)
[2024-03-26 07:24:02][INFO]: 收到 [uid] 信息 /终止(1711437837)
Task exception was never retrieved
future: <Task finished name='Task-137281' coro=<reply() done, defined at /home/opc/bilibili-chatbot/inko.py:37> exception=ResponseCodeException('你发送消息频率过快,请稍后再发~')>
Traceback (most recent call last):
  File "/home/opc/bilibili-chatbot/inko.py", line 60, in reply
    await session.reply(event, "脚本已暂停")
  File "/home/opc/.local/lib/python3.9/site-packages/bilibili_api/session.py", line 570, in reply
    return await send_msg(self.credential, event.sender_uid, msg_type, content)
  File "/home/opc/.local/lib/python3.9/site-packages/bilibili_api/session.py", line 301, in send_msg
    return await Api(**api, credential=credential).update_data(**data).result
  File "/home/opc/.local/lib/python3.9/site-packages/bilibili_api/utils/network.py", line 170, in result
    self.__result = await self.request()
  File "/home/opc/.local/lib/python3.9/site-packages/bilibili_api/utils/network.py", line 70, in inner
    return await func(*args, **kwargs)
  File "/home/opc/.local/lib/python3.9/site-packages/bilibili_api/utils/network.py", line 331, in request
    raise ResponseCodeException(code, msg, resp_data)
bilibili_api.exceptions.ResponseCodeException.ResponseCodeException: 接口返回错误代码:21020,信息:你发送消息频率过快,请稍后再发~。
{'code': 21020, 'message': '你发送消息频率过快,请稍后再发~', 'ttl': 1, 'data': None}
[2024-03-26 07:24:03][INFO]: 结束轮询

经过观察,这些多出来的日志记录的是此前发送过的消息内容,每条消息的时间戳也对应了它们接收/发送的真正时间。但这个现象出现得比较随机,目前没确定发生的条件,只能注意到是总在脚本运行了一段时间之后发生。

至于'code': 21020的报错应该只是连续触发回复导致的频率过快,不是关键问题。

一个很奇怪的问题是,这些消息在收到的时候已经正常触发过了自动回复: 图片

但它们仍然出现在了多余的日志当中,而且确确实实再次发送了回复: W7JF$J3SUR`1Q4GWQKWAAG

我推测是监听消息的时候出现了一些问题导致的重复接收。此处采用的轮询:

@session.on(Event.TEXT_)

我本身在做这个脚本之前没有编程经验,就算想找解决办法也不知道该搜什么关键词,可以说是束手无策了……不知道各位有没有什么头绪? 顺便,这个脚本是用了nohup挂在服务器长期运行的,不知道长时间的轮询是否是可能的原因?

我此前也在本项目内就这个脚本提出过其他问题,该问题内记录了我当时的完整代码,请参考: https://github.com/Nemo2011/bilibili-api/issues/601 目前的代码也都是建立在该框架上的,没有大的结构性改动。 如果这些信息不足以判断问题,我会把目前的完整代码加在此处。

Drelf2018 commented 6 months ago

终止后是重新启动过吗,怎么启动的呢?

xiaoxiaohaoa commented 6 months ago

终止后是重新启动过吗,怎么启动的呢?

在本例的这段引用中,终止(也就是session.close()结束轮询)之后没有重新启动过。

本例中,07:24:02时服务器接收到session.close()指令,并在同一时刻显示接收数条过往消息,接着在07:24:03正常执行指令内容结束轮询。

如果是指结束轮询后重新启动时使用的方法,我是每次都会手动重新运行脚本。

xiaoxiaohaoa commented 6 months ago

以及现在我严重怀疑我的脚本写得稀烂了……这段时间经常跑着跑着实例内存占用就爆了,然后服务器宕机一段时间……

图片

这里附上我写的完整代码,已隐去隐私信息。

import sys
f = open('a.log', 'a')
sys.stdout = f
sys.stderr = f # redirect std err, if necessary

import xlrd3
games = xlrd3.open_workbook(filename=r'games.xlsx')
gamelist = games.sheet_names()

sgames = xlrd3.open_workbook(filename=r'sgames.xlsx')
sgamelist = sgames.sheet_names()

import asyncio
from bilibili_api import Credential, sync, user
from bilibili_api.session import Session, Event, send_msg
from bilibili_api.user import User, RelationType
from bilibili_api.utils.picture import Picture

#登录凭据,记得改成自己的
SESSDATA = "※"
BILI_JCT = "※"
BUVID3 = "※"

credential = Credential(sessdata=SESSDATA, bili_jct=BILI_JCT, buvid3=BUVID3)
session = Session(credential)

list1 = [] #用于记录收到私信的粉丝的uid
list2 = [] #用于记录关注时间,作为判定取关并重新关注操作的凭据
list3 = [] #用于记录无视开关状态的白名单uid

with open('list1.txt',"r") as f: #读取已记录列表,该列表存储在本地,位置为指令执行时所处的目录
    for line in f:
        list1.append(line.strip('\n')) 

with open('list2.txt',"r") as f: #同上
    for line in f:
        list2.append(line.strip('\n'))

with open('list3.txt',"r") as f: #同上
    for line in f:
        list3.append(line.strip('\n'))

status = []

@session.on(Event.TEXT) #轮询,检测收到文字私信时触发
async def reply(event: Event):

    token = "1"

    code = event.content[-5:]
    name = event.content[:-5]

    uid = event.sender_uid #查询发信者的uid
    talker = User(uid, credential)
    rela = await talker.get_relation(uid)
    follow = rela['be_relation']['attribute'] #查询关注关系,此处描述的是对方对你的关注状态:0=未关注,2=已关注,6=互关,128=黑名单
    mtime = rela['be_relation']['mtime'] #关注时间,以时间戳的形式记录,未关注时为0,取关后重新关注会刷新,以此为凭据进行拉黑;需注意关系变为互相关注时也会刷新,后文单独拎出来讨论
    fid = str(uid) #转换为字符串
    ftime = fid + str(mtime) #同上

    if fid == "※":

        if event.content == "/终止": #杀死进程
            await session.reply(event, "脚本已停运")
            session.close()

        elif event.content == "/开始": #用于开始自动回复的口令,触发后将开始自动回复私信
            await session.reply(event, "脚本已开启")
            status.append(token)

        elif event.content == "/结束": #用于暂停自动回复的口令,触发后将不再自动回复私信
            await session.reply(event, "脚本已暂停")
            status.clear()

        elif event.content.isdigit():

            if event.content in list1:

                list1.remove(event.content)
                with open('list1.txt', "w") as f:
                    for element in list1:
                        f.write(element + "\n")

                await send_msg(credential=credential, msg_type=Event.TEXT, content="用户已移出黑名单", receiver_id=uid)

            else:

                await send_msg(credential=credential, msg_type=Event.TEXT, content="用户当前不在黑名单中,操作失败", receiver_id=uid)

    if token in status or fid in list3: #开关开启【或】发信人在白名单内

        if follow > 0 and follow < 128: #筛选:已关注用户

            if fid not in list1: #筛选:未通过关键词获取过回复的粉丝

                if event.content == "彩蛋": 
                    await session.reply(event, "恭喜你找到了一颗彩蛋!")

                elif name in gamelist: #正确的游戏关键词,接下来将进一步检测口令中的数字

                    gamex = games.sheet_by_name(sheet_name=name)
                    codelist = gamex.col_values(0)

                    if code in codelist: #口令正确,标志着对方成功获取回复;触发时发信人的uid和关注时间会随之被记录

                        def search(gamecode):

                            num_rows = gamex.nrows

                            for row in range(num_rows):
                                if gamex.cell_value(row, 0) == code:
                                    return gamex.cell_value(row, 1)
                            return None

                        password = search(code)

                        await session.reply(event, "非常感谢您下载本人汉化的游戏《" + name + "》。\n您的密码是:\n" + password) #正确关键词对应的回复

                        list1.extend([fid]) #记录发信粉丝的uid
                        with open("list1.txt","a+") as f: 
                            f.write(fid + "\n")

                        list2.extend([ftime]) #记录发信粉丝的uid和关注时间
                        with open("list2.txt","a+") as f: 
                            f.write(ftime + "\n")

            else: #曾通过关键词获取过回复的粉丝

                if ftime in list2 or follow == 6: #对方两次私信的关注时间一致(或关系为互相关注),说明对方自上次获取回复后没有取消关注过(或对方是你的好友),此时执行正常流程,跟上文一致,但不重复记录

                    if event.content == "彩蛋": 
                        await session.reply(event, "恭喜你找到了一颗彩蛋!")

                     #我需要治疗1.1

                    elif name in gamelist: #正确的游戏关键词,接下来将进一步检测口令中的数字

                        gamex = games.sheet_by_name(sheet_name=name)
                        codelist = gamex.col_values(0)

                        if code in codelist: #口令正确,标志着对方成功获取回复

                            def search(gamecode):

                                num_rows = gamex.nrows

                                for row in range(num_rows):
                                    if gamex.cell_value(row, 0) == code:
                                        return gamex.cell_value(row, 1)
                                return None

                            password = search(code)

                            await session.reply(event, "非常感谢您下载本人汉化的游戏《" + name + "》。\n您的密码是:\n" + password) #正确关键词对应的回复

                else: #对方两次私信的关注时间不一致且不是你的互关好友,说明对方自上次获取回复后曾取消关注
                    await session.reply(event, "获取密码功能已失效。") #可删除

                    #如果想要拉黑,复制粘贴这一句:【await talker.modify_relation(relation=RelationType.BLOCK)】

     #即使开关关闭、发送人也不在白名单时也能获取的游戏

    elif follow > 0 and follow < 128: #筛选:已关注用户

        if fid not in list1: #筛选:未通过关键词获取过回复的粉丝

            if event.content == "彩蛋α": 
                await session.reply(event, "恭喜你找到了一颗高级彩蛋!")

            elif name in sgamelist: #正确的关键词,接下来将进一步检测口令中的数字

                gamex = sgames.sheet_by_name(sheet_name=name)
                codelist = gamex.col_values(0)

                if code in codelist: #口令正确,标志着对方成功获取回复;触发时发信人的uid和关注时间会随之被记录

                    def search(gamecode):

                        num_rows = gamex.nrows

                        for row in range(num_rows):
                            if gamex.cell_value(row, 0) == code:
                                return gamex.cell_value(row, 1)
                        return None

                    password = search(code)

                    await session.reply(event, "非常感谢您下载本人汉化的游戏《" + name + "》。\n您的密码是:\n" + password) #正确关键词对应的回复

                    list1.extend([fid]) #记录发信粉丝的uid
                    with open("list1.txt","a+") as f: 
                        f.write(fid + "\n")

                    list2.extend([ftime]) #记录发信粉丝的uid和关注时间
                    with open("list2.txt","a+") as f: 
                        f.write(ftime + "\n")

        else: #曾通过关键词获取过回复的粉丝

            if ftime in list2 or follow == 6: #对方两次私信的关注时间一致(或关系为互相关注),说明对方自上次获取回复后没有取消关注过(或对方是你的好友),此时执行正常流程,跟上文一致,但不重复记录

                if event.content == "彩蛋α": 
                    await session.reply(event, "恭喜你找到了一颗高级彩蛋!")

                elif name in sgamelist: #正确的关键词,接下来将进一步检测口令中的数字

                    gamex = sgames.sheet_by_name(sheet_name=name)
                    codelist = gamex.col_values(0)

                    if code in codelist: #口令正确,标志着对方成功获取回复;触发时发信人的uid和关注时间会随之被记录

                        def search(gamecode):

                            num_rows = gamex.nrows

                            for row in range(num_rows):
                                if gamex.cell_value(row, 0) == code:
                                    return gamex.cell_value(row, 1)
                            return None

                        password = str(search(code))

                        await session.reply(event, "非常感谢您下载本人汉化的游戏《" + name + "》。\n您的密码是:\n" + password) #正确关键词对应的回复

            else: #对方两次私信的关注时间不一致且不是你的互关好友,说明对方自上次获取回复后曾取消关注
                    await session.reply(event, "获取密码功能已失效。") #可删除

sync(session.start())

除了会重复收信/回信,以及内存会爆以外,其他时候都运行得还算正常。

因为完全是靠网上东拼西凑来的内容堆出来的脚本,各种意义上都很屎,所以如果各位有任何建议,还望赐教。