Skip to content

Commit 01bcacf

Browse files
committed
🎨 添加向群推送bot所用pixiv账号关注画师的功能
1 parent a35207c commit 01bcacf

File tree

3 files changed

+196
-55
lines changed

3 files changed

+196
-55
lines changed

‎config.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,9 @@
1616

1717
CHAIN_REPLY = True # 是否启用合并转发回复模式
1818

19-
RANK_LIMIT = 5 # 每次推送排行榜时最多展示的作品数量
19+
RANK_LIMIT = 5 # 每次推送排行榜时最多展示的作品数量
20+
21+
# 是否启用“推送机器人账号关注的画师”功能
22+
# 开启后,各群管理员才能通过指令选择是否接收推送
23+
# 出于隐私和性能考虑,默认关闭
24+
ENABLE_FOLLOWING_SUBSCRIPTION = False

‎pixiv.py‎

Lines changed: 184 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from hoshino import Service, priv
1010
from hoshino.typing import CQEvent
1111
from pixivpy3 import AppPixivAPI
12-
from .config import PROXY_URL, MAX_DISPLAY_WORKS, IMAGE_QUALITY, CHECK_INTERVAL_HOURS
12+
from .config import PROXY_URL, MAX_DISPLAY_WORKS, IMAGE_QUALITY, CHECK_INTERVAL_HOURS, ENABLE_FOLLOWING_SUBSCRIPTION
1313
import aiohttp
1414

1515
# 插件配置
@@ -29,6 +29,8 @@
2929
[pixiv关闭r18] 屏蔽R18内容
3030
[pixiv屏蔽tag tag名] 屏蔽包含指定tag的作品
3131
[pixiv取消屏蔽tag tag名] 取消屏蔽指定tag
32+
[pixiv开启关注推送] 订阅机器人账号关注的全部画师
33+
[pixiv关闭关注推送] 取消订阅机器人账号关注的画师
3234
[pixiv群设置] 查看当前群���设置
3335
""".strip()
3436

@@ -113,8 +115,12 @@ def ensure_group_settings(self, group_id: str) -> None:
113115
self.subscriptions[group_id] = {
114116
'artists': [],
115117
'r18_enabled': False,
116-
'blocked_tags': []
118+
'blocked_tags': [],
119+
'push_following_enabled': False
117120
}
121+
# 兼容旧配置,如果旧配置没有这个键则添加默认值
122+
elif 'push_following_enabled' not in self.subscriptions[group_id]:
123+
self.subscriptions[group_id]['push_following_enabled'] = False
118124

119125
def add_subscription(self, group_id: str, user_id: str) -> bool:
120126
"""添加订阅"""
@@ -152,6 +158,18 @@ def is_r18_enabled(self, group_id: str) -> bool:
152158
return self.subscriptions[group_id].get('r18_enabled', False)
153159
return False
154160

161+
def set_push_following(self, group_id: str, enabled: bool) -> None:
162+
"""设置群的 关注画师推送 开关"""
163+
self.ensure_group_settings(group_id)
164+
self.subscriptions[group_id]['push_following_enabled'] = enabled
165+
self.save_subscriptions()
166+
167+
def is_push_following_enabled(self, group_id: str) -> bool:
168+
"""检查群是否开启了 关注画师推送"""
169+
if group_id in self.subscriptions:
170+
return self.subscriptions[group_id].get('push_following_enabled', False)
171+
return False
172+
155173
def add_blocked_tag(self, group_id: str, tag: str) -> bool:
156174
"""添加屏蔽tag"""
157175
self.ensure_group_settings(group_id)
@@ -179,13 +197,9 @@ def get_blocked_tags(self, group_id: str) -> List[str]:
179197

180198
def get_group_settings(self, group_id: str) -> Dict:
181199
"""获取群设置"""
182-
if group_id in self.subscriptions:
183-
return self.subscriptions[group_id]
184-
return {
185-
'artists': [],
186-
'r18_enabled': False,
187-
'blocked_tags': []
188-
}
200+
self.ensure_group_settings(group_id)
201+
return self.subscriptions[group_id]
202+
189203

190204
def is_illust_allowed(self, illust: dict, group_id: Union[str, int]) -> bool:
191205
"""检查作品是否允许在指定群推送"""
@@ -332,6 +346,48 @@ async def user_illusts(self, user_id: Union[str, int]):
332346
sv.logger.error(f"获取Pixiv用户作品列表时发生未知异常 '{user_id}': {e}")
333347
return {}, {}
334348

349+
async def get_illust_follow(self, start_time: datetime, interval_hours: float) -> List[Dict]:
350+
"""
351+
获取当前bot关注画师在指定时间窗口内的新作品。
352+
API本身返回最近作品,此函数在此基础上进行时间过滤。
353+
"""
354+
try:
355+
# 调用API获取原始的关注动态列表
356+
result = await self.__exec_and_retry_with_login(
357+
self.api.illust_follow
358+
)
359+
360+
# 检查API返回是否有效
361+
if not isinstance(result, dict) or 'illusts' not in result or not result.get('illusts'):
362+
sv.logger.error(f"获取Pixiv关注作品列表失败或列表为空: {result}")
363+
return [] # 失败或无内容时返回空列表
364+
365+
# 准备时间和用于存放结果的容器
366+
check_start = start_time - timedelta(hours=interval_hours)
367+
check_end = start_time
368+
new_illusts_in_window = []
369+
370+
# 遍历API返回的所有作品,并根据时间窗口进行过滤
371+
for illust in result['illusts']:
372+
try:
373+
# 解析作品创建时间字符串
374+
create_date_utc = datetime.fromisoformat(illust['create_date']).astimezone(timezone.utc)
375+
376+
# 判断作品是否在检查时间窗口内
377+
if check_start < create_date_utc <= check_end:
378+
new_illusts_in_window.append(illust)
379+
380+
except (ValueError, TypeError, KeyError) as e:
381+
sv.logger.warning(f"解析或过滤关注作品时跳过一个项目: {e}, 作品ID: {illust.get('id')}")
382+
continue
383+
# 返回经过时间过滤后的新作品列表
384+
return new_illusts_in_window
385+
386+
except Exception as e:
387+
sv.logger.error(f"获取Pixiv关注作品时发生未知异常: {e}")
388+
return [] # 确保任何未知异常都返回一个安全的空列表
389+
390+
335391
@staticmethod
336392
async def download_image_as_base64(url: str) -> str:
337393
"""下载图片并转换为base64编码"""
@@ -524,6 +580,31 @@ async def set_pixiv_token(bot, ev: CQEvent):
524580
success, msg = manager.login(refresh_token)
525581
await bot.send(ev, msg)
526582

583+
@sv.on_prefix('pixiv开启关注推送')
584+
async def enable_push_following(bot, ev: CQEvent):
585+
"""开启机器人账号关注画师的推送 (仅管理员)"""
586+
if not priv.check_priv(ev, priv.ADMIN):
587+
await bot.send(ev, "只有群主或管理员才能设置此项")
588+
return
589+
590+
if not ENABLE_FOLLOWING_SUBSCRIPTION:
591+
await bot.send(ev, "该功能已被维护组全局关闭")
592+
return
593+
594+
group_id = str(ev.group_id)
595+
manager.set_push_following(group_id, True)
596+
await bot.send(ev, "本群将会收到账号关注画师的更新")
597+
598+
@sv.on_prefix('pixiv关闭关注推送')
599+
async def disable_push_following(bot, ev: CQEvent):
600+
"""关闭机器人账号关注画师的推送 (仅管理员)"""
601+
if not priv.check_priv(ev, priv.ADMIN):
602+
await bot.send(ev, "只有群主或管理员才能设置此项")
603+
return
604+
605+
group_id = str(ev.group_id)
606+
manager.set_push_following(group_id, False)
607+
await bot.send(ev, "已关闭关注推送")
527608

528609
@sv.on_prefix('pixiv开启r18')
529610
async def enable_r18(bot, ev: CQEvent):
@@ -597,17 +678,18 @@ async def show_group_settings(bot, ev: CQEvent):
597678
msg += f"📋 订阅画师数量: {len(settings['artists'])}\n"
598679
msg += f"🔞 R18推送: {'开启' if settings['r18_enabled'] else '关闭'}\n"
599680

681+
if ENABLE_FOLLOWING_SUBSCRIPTION:
682+
following_status = '开启' if settings.get('push_following_enabled', False) else '关闭'
683+
msg += f"💖 关注画师推送: {following_status}\n"
684+
600685
blocked_tags = settings['blocked_tags']
601686
if blocked_tags:
602687
msg += f"🚫 屏蔽tag: {', '.join(blocked_tags)}"
603688
else:
604689
msg += "🚫 屏蔽tag: 无"
605-
606690
await bot.send(ev, msg)
607691

608692

609-
610-
611693
@sv.on_prefix('pixiv强制检查')
612694
async def force_check_updates(bot, ev: CQEvent):
613695
"""强制执行一次更新检查 (仅用于测试)"""
@@ -660,16 +742,64 @@ async def construct_group_message(artist_name: str, filtered_illusts: List[Dict]
660742
return ''.join(msg_parts)
661743

662744

745+
async def process_and_send_updates(bot, user_id: str, artist_name: str, new_illusts: List[Dict], target_group_ids: set):
746+
"""
747+
一个辅助函数, 负责处理单个画师的更新并发送给所有目标群组。
748+
发送单个画师的新作, 为每个群组每个独立过滤作品、构造消息并发送。
749+
750+
:param bot: Bot实例
751+
:param user_id: 画师ID
752+
:param artist_name: 画师名字
753+
:param new_illusts: 该画师的新作品列表
754+
:param target_group_ids: 需要被通知的群组ID集合
755+
"""
756+
if not new_illusts:
757+
return # 如果没有新作品,直接返回
758+
759+
# 向所有订阅了该画师的群组发送消息(根据群设置过滤内容)
760+
for group_id in target_group_ids:
761+
try:
762+
# 根据群设置过滤作品
763+
filtered_illusts = [
764+
illust for illust in new_illusts if manager.is_illust_allowed(illust, group_id)
765+
]
766+
767+
# 如果过滤后没有符合条件的作品,则跳过这个群
768+
if not filtered_illusts:
769+
continue
770+
771+
await bot.send_group_msg(
772+
group_id=int(group_id),
773+
message=await construct_group_message(artist_name, filtered_illusts)
774+
)
775+
# 避免发送消息过快被限制
776+
await asyncio.sleep(1)
777+
778+
except Exception as e:
779+
sv.logger.error(f"向群 {group_id} 发送画师 {user_id} ({artist_name}) 更新消息时出错: {e}")
780+
continue
781+
663782
@sv.scheduled_job('interval', hours=CHECK_INTERVAL_HOURS)
664783
async def check_updates():
784+
"""
785+
发送画师订阅的更新作品到对应群组的任务
786+
787+
实现思路:
788+
1. user_follow的获取到的画师更新的作品实际上是和在当前时间窗口内用画师ID获取的作品列表是一样的, 所以需要去重
789+
2. 根据避免频繁请求API的原则, 对每个画师只请求一次, 也就是说在user_follow推送之后就不需要用画师ID去请求一次了
790+
3. 构建一个画师ID到订阅群列表的映射表
791+
4. user_follow获取到时间窗口内的更新之后, 根据群设置过滤内容, 然后根据群是否���阅该画师和是否推送bot关注画师为条件来决定是否发送消息,
792+
将发送过的画师ID从映射表中删除
793+
5. 剩下的画师ID再用画师ID去请求一次, 这样就避免了重复请求和重复发送消息的问题
794+
"""
665795
start_time = datetime.now()
666796

667797
bot = nonebot.get_bot()
668798

669799
# 计算本次检查的时间窗口 - 以当前时间为结束点,向前检查CHECK_INTERVAL_HOURS的小时数
670800
check_time = datetime.now(timezone.utc)
671801

672-
# 收集��有需要检查的画师ID,并记录哪些群订阅了哪些画师
802+
# 收集所有需要检查的画师ID,并记录画师被哪些群订阅
673803
artist_to_groups = {} # {artist_id: [group_id1, group_id2, ...]}
674804

675805
for group_id, group_data in manager.subscriptions.items():
@@ -679,54 +809,60 @@ async def check_updates():
679809
artist_to_groups[user_id] = []
680810
artist_to_groups[user_id].append(group_id)
681811

682-
if not artist_to_groups: # 没有订阅任何画师
683-
return
812+
# 处理关注推送 (如果开启)
813+
if ENABLE_FOLLOWING_SUBSCRIPTION:
814+
groups_enabling_following = {
815+
group_id for group_id, setting in manager.subscriptions.items()
816+
if setting.get('push_following_enabled', False)
817+
}
818+
819+
# 获取关注画师在时间窗口内的新作品
820+
followed_illusts = await manager.get_illust_follow(
821+
start_time=check_time,
822+
interval_hours=CHECK_INTERVAL_HOURS
823+
)
684824

685-
# 对每个画师只请求一次
825+
# 按画师ID分组作品
826+
bot_followed_illusts = {}
827+
for illust in followed_illusts:
828+
user_id = str(illust['user']['id'])
829+
if user_id not in bot_followed_illusts:
830+
bot_followed_illusts[user_id] = {'user': illust['user'], 'illusts': []}
831+
bot_followed_illusts[user_id]['illusts'].append(illust)
832+
833+
# 处理并发送关注画师的更新
834+
for user_id, data in bot_followed_illusts.items():
835+
artist_name = data['user']['name']
836+
new_illusts = data['illusts']
837+
838+
# 计算需要通知的所有群组:订阅了该画师的 + 开启了全局关注推送的
839+
target_group_ids = set(artist_to_groups.get(user_id, [])) | groups_enabling_following
840+
841+
await process_and_send_updates(bot, user_id, artist_name, new_illusts, target_group_ids)
842+
843+
# 从待检查列表中移除,避免重复请求
844+
if user_id in artist_to_groups:
845+
del artist_to_groups[user_id]
846+
847+
# 处理剩下的、未被关注推送覆盖的画师
686848
for user_id, group_ids in artist_to_groups.items():
687849
try:
688-
# 使用精确的时间窗口获取新作品
689850
user_info, new_illusts = await manager.get_new_illusts_with_user_info(
690851
user_id,
691852
start_time=check_time,
692853
interval_hours=CHECK_INTERVAL_HOURS
693854
)
694855

695-
artist_name = user_info['name'] if user_info else f"画师ID:{user_id}"
696-
697-
# 如果没有新作品,跳过
698856
if not new_illusts:
699-
sv.logger.info(f"{artist_name} 没有新作品,跳过")
700-
await asyncio.sleep(3) # 避免频繁请求API
857+
sv.logger.info(f"画师 {user_id} 没有新作品,跳过")
858+
await asyncio.sleep(3)
701859
continue
702860

703-
# 向所有订阅了该画师的群组发送消息(根据群设置过滤内容)
704-
for group_id in group_ids:
705-
try:
706-
# 根据群设置过滤作品
707-
filtered_illusts = []
708-
for illust in new_illusts:
709-
is_allowed = manager.is_illust_allowed(illust, group_id)
710-
if is_allowed:
711-
filtered_illusts.append(illust)
712-
713-
# 如果过滤后没有作品,跳过这个群
714-
if not filtered_illusts:
715-
continue
716-
717-
await bot.send_group_msg(
718-
group_id=int(group_id),
719-
message=await construct_group_message(artist_name, filtered_illusts)
720-
)
721-
# 避免发送消息过快被限制
722-
await asyncio.sleep(1)
723-
724-
except Exception as e:
725-
sv.logger.error(f"向群 {group_id} 发送画师 {user_id} 更新消息时出错: {e}")
726-
continue
861+
artist_name = user_info.get('name', f"画师ID:{user_id}")
862+
863+
await process_and_send_updates(bot, user_id, artist_name, new_illusts, set(group_ids))
727864

728-
# 避免频繁请求API
729-
sv.logger.info(f"画师 {user_id} 处理完成,等待5秒...")
865+
sv.logger.info(f"画师 {user_id} 处理完成,等待3秒...")
730866
await asyncio.sleep(3)
731867
except Exception as e:
732868
sv.logger.error(f"获取画师 {user_id} 更新时出错: {e}")

‎pixiv_tools.py‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,12 @@
2828
HELP = '''
2929
[预览画师 画师ID/画师URL] 预览画师最新作品
3030
[获取插画|pget 作品ID/作品URL] 通过作品ID或URL获取指定作品
31-
[插画日榜] 获取Pixiv插画日榜
32-
[插画男性向排行] 获取Pixiv插画男性向排行榜
33-
[插画女性向排行] 获取Pixiv插画女性向排行榜
34-
[插画周榜] 获取Pixiv插画周榜
35-
[插画月榜] 获取Pixiv插画月榜
36-
[插画原画榜] 获取Pixiv插画原画榜
31+
[插画日榜] 获取插画日榜
32+
[插画男性向排行] 获取插画男性向排行榜
33+
[插画女性向排行] 获取插画女性向排行榜
34+
[插画周榜] 获取插画周榜
35+
[插画月榜] 获取插画月榜
36+
[插画原画榜] 获取插画原画榜
3737
'''.strip()
3838

3939
sv = Service(

0 commit comments

Comments
 (0)