diff --git a/starbot/commands/builtin/__init__.py b/starbot/commands/builtin/__init__.py index d7979da..c2ac11c 100644 --- a/starbot/commands/builtin/__init__.py +++ b/starbot/commands/builtin/__init__.py @@ -14,3 +14,4 @@ import starbot.commands.builtin.enable_command import starbot.commands.builtin.help import starbot.commands.builtin.ranking.ranking import starbot.commands.builtin.ranking.ranking_double +import starbot.commands.builtin.resend diff --git a/starbot/commands/builtin/resend.py b/starbot/commands/builtin/resend.py new file mode 100644 index 0000000..0e944f3 --- /dev/null +++ b/starbot/commands/builtin/resend.py @@ -0,0 +1,36 @@ +from graia.ariadne import Ariadne +from graia.ariadne.event.message import FriendMessage +from graia.ariadne.message.chain import MessageChain +from graia.ariadne.message.parser.twilight import Twilight, FullMatch, UnionMatch +from graia.ariadne.model import Friend +from graia.saya import Channel +from graia.saya.builtins.broadcast import ListenerSchema + +from ...core.datasource import DataSource +from ...utils import config + +prefix = config.get("COMMAND_PREFIX") +master = config.get("MASTER_QQ") + +channel = Channel.current() + + +@channel.use( + ListenerSchema( + listening_events=[FriendMessage], + inline_dispatchers=[Twilight( + FullMatch(prefix), + UnionMatch("补发", "resend") + )], + ) +) +async def resend(app: Ariadne, friend: Friend): + if friend.id != master: + return + + if not config.get("BAN_RESEND"): + await app.send_friend_message(friend, MessageChain("补发功能未启用~")) + + datasource: DataSource = app.options["StarBotDataSource"] + bot = datasource.get_bot(app.account) + await bot.resend() diff --git a/starbot/core/bot.py b/starbot/core/bot.py index c2421ac..023ebfe 100644 --- a/starbot/core/bot.py +++ b/starbot/core/bot.py @@ -182,6 +182,10 @@ class StarBot: if len(need_follow_uids) > 0: asyncio.create_task(follow_task(need_follow_uids)) + # 检测消息补发配置完整性 + if config.get("BAN_RESEND") and config.get("MASTER_QQ") is None: + logger.warning("检测到风控消息补发功能已开启, 但未配置机器人主人 QQ, 将会导致 \"补发\" 命令无法使用, 请使用 config.set(\"MASTER_QQ\", QQ号) 进行配置") + # 启动消息推送模块 Ariadne.options["default_account"] = self.__datasource.bots[0].qq diff --git a/starbot/core/sender.py b/starbot/core/sender.py index a0fb0ca..f449bce 100644 --- a/starbot/core/sender.py +++ b/starbot/core/sender.py @@ -43,7 +43,7 @@ class Bot(BaseModel): __banned: Optional[bool] = PrivateAttr() """当前是否被风控""" - __queue: Optional[List[Message]] = PrivateAttr() + __queue: Optional[List[Tuple[int, MessageChain, int]]] = PrivateAttr() """消息补发队列""" def __init__(self, **data: Any): @@ -66,15 +66,78 @@ class Bot(BaseModel): for up in self.ups: up.inject_bot(self) + async def resend(self): + """ + 风控消息补发 + """ + if len(self.__queue) == 0: + if config.get("MASTER_QQ"): + await self.__bot.send_friend_message(config.get("MASTER_QQ"), "补发队列为空~") + return + + task_start_tip = f"补发任务已启动, 补发队列长度: {len(self.__queue)}" + logger.info(task_start_tip) + if config.get("MASTER_QQ"): + await self.__bot.send_friend_message(config.get("MASTER_QQ"), f"{task_start_tip}~") + + while len(self.__queue) > 0: + msg_id, message, timestamp = self.__queue[0] + resend_time_limit = config.get("RESEND_TIME_LIMIT") + if resend_time_limit != 0 and int(time.time()) - timestamp > resend_time_limit: + logger.info(f"消息已超时, 跳过补发, 群({msg_id}) : {message.safe_display}") + self.__queue.pop(0) + continue + try: + await self.__bot.send_group_message(msg_id, message) + self.__banned = False + logger.info(f"{self.qq} -> 群[{msg_id}] : {message.safe_display}") + self.__queue.pop(0) + await asyncio.sleep(3) + except AccountMuted: + logger.warning(f"Bot({self.qq}) 在群 {msg_id} 中被禁言") + self.__queue.pop(0) + continue + except UnknownTarget: + self.__queue.pop(0) + continue + except RemoteException as ex: + if "AT_ALL_LIMITED" in str(ex): + logger.warning(f"Bot({self.qq}) 今日的@全体成员次数已达到上限") + self.__queue.pop(0) + self.__at_all_limited = time.localtime(time.time()).tm_yday + continue + elif "LIMITED_MESSAGING" in str(ex): + self.__banned = True + logger.error(f"消息补发期间再次触发风控, 需人工再次通过验证码验证") + if not config.get("BAN_CONTINUE_SEND_MESSAGE"): + logger.warning("已停止尝试消息推送, 后续消息将会被暂存, 请人工通过验证码验证后使用 \"补发\" 命令恢复") + if config.get("MASTER_QQ"): + notice = "消息补发期间再次触发验证, 请手动通过验证码验证后重新发送 \"补发\" 命令~" + await self.__bot.send_friend_message(config.get("MASTER_QQ"), notice) + else: + logger.warning("未设置主人 QQ, 无法发送提醒消息, 可使用 config.set(\"MASTER_QQ\", QQ号) 进行设置") + return + else: + logger.exception("消息推送模块异常", ex) + if config.get("MASTER_QQ"): + await self.__bot.send_friend_message(config.get("MASTER_QQ"), "补发任务期间出现异常, 详细请查看日志~") + return + except Exception as ex: + logger.exception("消息推送模块异常", ex) + if config.get("MASTER_QQ"): + await self.__bot.send_friend_message(config.get("MASTER_QQ"), "补发任务期间出现异常, 详细请查看日志~") + return + + logger.success(f"补发任务已完成") + if config.get("MASTER_QQ"): + await self.__bot.send_friend_message(config.get("MASTER_QQ"), "补发任务已完成~") + async def send_message(self, msg: Message): """ 消息发送 Args: msg: Message 实例 - - Raises: - """ if msg.type == PushType.Friend: for message in msg.get_message_chains(): @@ -89,7 +152,19 @@ class Bot(BaseModel): for message in msgs: try: + if self.__banned and config.get("BAN_RESEND") and not config.get("BAN_CONTINUE_SEND_MESSAGE"): + if not config.get("RESEND_AT_MESSAGE"): + message = message.exclude(At, AtAll) + if len(message) > 0: + self.__queue.append((msg.id, message, msg.get_time())) + logger.error(f"受风控影响, 要发送的消息已暂存, 请人工通过验证码验证后使用 \"补发\" 命令恢复, 群号: {msg.id}") + if config.get("MASTER_QQ"): + await self.__bot.send_friend_message(config.get("MASTER_QQ"), config.get("BAN_NOTICE")) + else: + logger.warning("未设置主人 QQ, 无法发送提醒消息, 可使用 config.set(\"MASTER_QQ\", QQ号) 进行设置") + continue await self.__bot.send_group_message(msg.id, message) + self.__banned = False logger.info(f"{self.qq} -> 群[{msg.id}] : {message.safe_display}") except AccountMuted: logger.warning(f"Bot({self.qq}) 在群 {msg.id} 中被禁言") @@ -103,7 +178,15 @@ class Bot(BaseModel): self.__at_all_limited = time.localtime(time.time()).tm_yday continue elif "LIMITED_MESSAGING" in str(ex): + self.__banned = True logger.error(f"受风控影响, 发送群消息失败, 需人工通过验证码验证, 群号: {msg.id}") + if config.get("BAN_RESEND"): + if not config.get("RESEND_AT_MESSAGE"): + message = message.exclude(At, AtAll) + if len(message) > 0: + self.__queue.append((msg.id, message, msg.get_time())) + if not config.get("BAN_CONTINUE_SEND_MESSAGE"): + logger.warning("已停止尝试消息推送, 后续消息将会被暂存, 请人工通过验证码验证后使用 \"补发\" 命令恢复") if config.get("MASTER_QQ"): await self.__bot.send_friend_message(config.get("MASTER_QQ"), config.get("BAN_NOTICE")) else: @@ -115,7 +198,7 @@ class Bot(BaseModel): logger.exception("消息推送模块异常", ex) continue - if exception is not None: + if exception is not None and not self.__banned: message = "" if isinstance(exception, AtAllLimitedException): message = config.get("AT_ALL_LIMITED_MESSAGE") diff --git a/starbot/utils/config.py b/starbot/utils/config.py index 73a2d28..f2ca98a 100644 --- a/starbot/utils/config.py +++ b/starbot/utils/config.py @@ -1,9 +1,11 @@ -from loguru import logger import json from typing import Any, Optional + +from loguru import logger + from ..exception.CredentialFromJSONException import CredentialFromJSONException -SIMPLE_CONFIG = { +DEFAULT_CONFIG = { # 是否检测最新 StarBot 版本 "CHECK_VERSION": True, @@ -99,126 +101,6 @@ SIMPLE_CONFIG = { # HTTP 代理 "PROXY": "", - # 是否使用 HTTP API 推送 - "USE_HTTP_API": False, - # HTTP API 端口 - "HTTP_API_PORT": 8088, - # 默认 HTTP API 推送 Bot QQ,多 Bot 推送时必填 - "HTTP_API_DEAFULT_BOT": None, - - # 命令触发前缀 - "COMMAND_PREFIX": "", - # 每个群开播 @ 我命令人数上限,单次 @ 人数过多容易被风控,不推荐修改 - "COMMAND_LIVE_ON_AT_ME_LIMIT": 20, - # 每个群动态 @ 我命令人数上限,单次 @ 人数过多容易被风控,不推荐修改 - "COMMAND_DYNAMIC_AT_ME_LIMIT": 20, - - # 被风控时,发送给主人 QQ 的提醒消息 - "BAN_NOTICE": "发送消息失败, 请手动通过验证码验证~", - # 是否启用风控消息补发,暂未实现 - "BAN_RESEND": False, - # 风控发送失败消息滞留时间上限,消息因风控滞留超出此时长不会进行补发,0 为无限制,单位:秒,暂未实现 - "RESEND_TIME_LIMIT": 0, - # 是否补发开播推送、下播推送、直播报告、动态推送中的 @全体成员 和 @群成员 消息,可能造成不必要的打扰,不推荐开启,暂未实现 - "RESEND_AT_MESSAGE": False -} - -FULL_CONFIG = { - # 是否检测最新 StarBot 版本 - "CHECK_VERSION": True, - - # Redis 连接配置 ( 必须 ) - # Redis 地址 - "REDIS_HOST": "localhost", - # Redis 端口 - "REDIS_PORT": 6379, - # Redis 数据库号 - "REDIS_DB": 0, - # Redis 用户名 - "REDIS_USERNAME": None, - # Redis 密码 - "REDIS_PASSWORD": None, - - # MySQL 数据源连接配置 ( 可选 ) - # MySQL 地址 - "MYSQL_HOST": "localhost", - # MySQL 端口 - "MYSQL_PORT": 3306, - # MySQL 用户名 - "MYSQL_USERNAME": "root", - # MySQL 密码 - "MYSQL_PASSWORD": "123456", - # MySQL 数据库名 - "MYSQL_DB": "starbot", - - # Mirai HTTP 及 Websocket 端口 - "MIRAI_PORT": 7827, - - # 登录 B 站账号所需 Cookie 数据 ( 不登录账号将有部分功能不可用 ) 各字段获取方式查看:https://bot.starlwr.com/depoly/document - "SESSDATA": None, - "BILI_JCT": None, - "BUVID3": None, - - # 以上 Cookie 数据所对应的 B 站账号 UID,自动关注打开了动态推送但没有关注的用户时使用 - "ACCOUNT_UID": None, - # 是否自动关注打开了动态推送但没有关注的用户,推荐打开,否则无法获取未关注用户的动态更新信息 - "AUTO_FOLLOW_OPENED_DYNAMIC_UPDATE_UP": True, - - # 是否将日志同时输出到文件中 - "LOG_TO_FILE": False, - - # 连接每个直播间的间隔等待时长,用于避免连接大量直播间时的并发过多异常 too many file descriptors in select(),单位:秒 - "CONNECTION_INTERVAL": 0.2, - # 成功连接所有主播直播间的最大等待时长,可使得日志输出顺序更加易读,一般无需修改此处,设置为 0 会自适应计算,单位:秒 - "WAIT_FOR_ALL_CONNECTION_TIMEOUT": 0, - - # 是否自动判断仅连接必要的直播间,即当某直播间的开播、下播、直播报告开关均未开启时,自动跳过连接直播间,以节省性能 - "ONLY_CONNECT_NECESSARY_ROOM": False, - # 是否自动判断仅处理必要的直播事件,例如当某直播间的下播推送和直播报告中均不包含弹幕相关功能,则不再处理此直播间的弹幕事件,以节省性能 - "ONLY_HANDLE_NECESSARY_EVENT": False, - - # 主播下播后再开播视为主播网络波动断线重连的时间间隔,在此时间内重新开播不会重新计算本次直播数据,且不重复 @全体成员,单位:秒 - "UP_DISCONNECT_CONNECT_INTERVAL": 120, - # 视为主播网络波动断线重连时,需发送的额外提示消息 - "UP_DISCONNECT_CONNECT_MESSAGE": "检测到下播后短时间内重新开播,可能是由于主播网络波动引起,本次开播不再重复通知", - - # 当日 @全体成员 达到上限后,需发送的额外提示消息 - "AT_ALL_LIMITED_MESSAGE": "今日@全体成员次数已达上限,将尝试@指定成员,请需要接收单独@消息的群员使用\"开播@我\"命令进行订阅", - # 设置了 @全体成员 但没有管理员权限时,需发送的额外提示消息 - "NO_PERMISSION_MESSAGE": "已设置@全体成员但没有管理员权限,将尝试@指定成员,请需要接收单独@消息的群员使用\"开播@我\"命令进行订阅", - - # 动态推送抓取频率和视为新动态的时间间隔,单位:秒 - "DYNAMIC_INTERVAL": 10, - - # 绘图器普通字体路径,如需自定义,请将字体放入 resource 文件夹中后,修改配置中的 normal.ttf 为您的字体文件名 - "PAINTER_NORMAL_FONT": "normal.ttf", - # 绘图器粗体字体路径,如需自定义,请将字体放入 resource 文件夹中后,修改配置中的 bold.ttf 为您的字体文件名 - "PAINTER_BOLD_FONT": "bold.ttf", - # 绘图器自适应不覆盖已绘制图形的间距,单位:像素 - "PAINTER_AUTO_SIZE_BY_LIMIT_MARGIN": 10, - - # 弹幕词云字体路径,如需自定义,请将字体放入 resource 文件夹中后,修改配置中的 cloud.ttf 为您的字体文件名 - "DANMU_CLOUD_FONT": "cloud.ttf", - # 弹幕词云图片背景色 - "DANMU_CLOUD_BACKGROUND_COLOR": "white", - # 弹幕词云最大字号 - "DANMU_CLOUD_MAX_FONT_SIZE": 200, - # 弹幕词云最多词数 - "DANMU_CLOUD_MAX_WORDS": 80, - # 弹幕词云停用词路径,存储时每行一个停用词,以纯文本方式存储,可过滤这些词使其不出现在词云中 - "DANMU_CLOUD_STOP_WORDS": "", - # 弹幕词云自定义词典路径,存储时每行一个词,以纯文本方式存储,在对弹幕进行切词时,词典中的词不会被切分开 - "DANMU_CLOUD_DICT": "", - - # 需加载的用户自定义命令包 - "CUSTOM_COMMANDS_PACKAGE": None, - - # Bot 主人 QQ,用于接收 Bot 异常通知等 - "MASTER_QQ": None, - - # HTTP 代理 - "PROXY": "", - # 是否使用 HTTP API 推送 "USE_HTTP_API": False, # HTTP API 端口 @@ -235,16 +117,16 @@ FULL_CONFIG = { # 被风控时,发送给主人 QQ 的提醒消息 "BAN_NOTICE": "发送消息失败, 请手动通过验证码验证~", - # 是否启用风控消息补发,暂未实现 + # 是否启用风控消息补发,启用后,因风控导致发送失败的推送消息会被暂存,解除风控后使用 ”补发“ 命令可以补发暂存的消息 "BAN_RESEND": True, - # 风控发送失败消息滞留时间上限,消息因风控滞留超出此时长不会进行补发,0 为无限制,单位:秒,暂未实现 + # 启用风控消息补发时,被风控时是否继续尝试发送消息,关闭后,发生风控时,后续需要发送的消息将不再尝试发送,而是被直接暂存,需使用 ”补发“ 命令后恢复正常,用于防止风控期间频繁尝试发送消息导致更严重的冻结 + "BAN_CONTINUE_SEND_MESSAGE": True, + # 风控发送失败消息滞留时间上限,消息因风控滞留超出此时长不会进行补发,0 为无限制,单位:秒 "RESEND_TIME_LIMIT": 0, - # 是否补发开播推送、下播推送、直播报告、动态推送中的 @全体成员 和 @群成员 消息,可能造成不必要的打扰,不推荐开启,暂未实现 + # 是否补发开播推送、下播推送、直播报告、动态推送中的 @全体成员 和 @群成员 消息,可能造成不必要的打扰,不推荐开启 "RESEND_AT_MESSAGE": False } -use_config = SIMPLE_CONFIG - user_config = {} @@ -258,53 +140,9 @@ def use(**config: Any): user_config.update(config) -def use_simple_config(): - """ - 使用最简配置,默认配置如下: - - 自动检测最新版本 - 使用 Redis 默认连接配置 (host: "localhost", port: 6379, db: 0, username: "", password: "") - 使用 MySQL 默认连接配置 (host: "localhost", port: 3306, db: "starbot", username: "root", password: "123456") - Mirai 连接端口 7827 - 未设置登录 B 站账号所需 Cookie 数据 - 不启用节省性能优化 - 主播下播后 2 分钟内开播视为主播网络波动,并发送提示消息 "检测到下播后短时间内重新开播,可能是由于主播网络波动引起,本次开播不再重复通知" - 未设置 Bot 主人 QQ - 不使用 HTTP 代理 - 不开启 HTTP API 推送 - 消息发送间隔 0.5 秒 - 无命令触发前缀 - 不开启风控消息补发 - """ - global use_config - use_config = SIMPLE_CONFIG - - -def use_full_config(): - """ - 使用推荐配置,默认配置如下: - - 自动检测最新版本 - 使用 Redis 默认连接配置 (host: "localhost", port: 6379, db: 0, username: "", password: "") - 使用 MySQL 默认连接配置 (host: "localhost", port: 3306, db: "starbot", username: "root", password: "123456") - Mirai 连接端口 7827 - 未设置登录 B 站账号所需 Cookie 数据 - 不启用节省性能优化 - 主播下播后 2 分钟内开播视为主播网络波动,并发送提示消息 "检测到下播后短时间内重新开播,可能是由于主播网络波动引起,本次开播不再重复通知" - 未设置 Bot 主人 QQ - 不使用 HTTP 代理 - 开启 HTTP API 推送 (port: 8088) - 消息发送间隔 0.5 秒 - 无命令触发前缀 - 开启风控消息补发 仅补发推送消息 - """ - global use_config - use_config = FULL_CONFIG - - def set_credential(sessdata: str, bili_jct: str, buvid3: str): """ - 设置登录 B 站账号所需 Cookie 数据,各字段获取方式查看:https://bili.moyu.moe/#/get-credential.md + 设置登录 B 站账号所需 Cookie 数据,各字段获取方式查看:https://bot.starlwr.com/depoly/document Args: sessdata: SESSDATA @@ -315,6 +153,7 @@ def set_credential(sessdata: str, bili_jct: str, buvid3: str): set("BILI_JCT", bili_jct) set("BUVID3", buvid3) + def set_credential_from_json(json_file: Optional[str] = None, json_str: Optional[str] = None): """ 从JSON读取B站credential @@ -345,12 +184,13 @@ def set_credential_from_json(json_file: Optional[str] = None, json_str: Optional logger.error("提供的B站凭据 JSON 字符串格式不正确") raise CredentialFromJSONException("提供的B站凭据 JSON 字符串格式不正确") - set("SESSDATA",config["sessdata"]) - set("BILI_JCT",config["bili_jct"]) - set("BUVID3",config["buvid3"]) + set("SESSDATA", config["sessdata"]) + set("BILI_JCT", config["bili_jct"]) + set("BUVID3", config["buvid3"]) logger.success("成功从JSON中导入了B站凭据") + def get(key: str) -> Any: """ 获取配置项的值 @@ -361,7 +201,7 @@ def get(key: str) -> Any: Returns: 配置项的值 """ - return user_config[key] if key in user_config else use_config[key] + return user_config[key] if key in user_config else DEFAULT_CONFIG[key] def set(key: str, value: Any):