feat: Group message banned resend support

This commit is contained in:
LWR 2023-06-20 01:13:13 +08:00
parent 4d17c2d3b8
commit f712c278d4
5 changed files with 145 additions and 181 deletions

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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")

View File

@ -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):