99from hoshino import Service , priv
1010from hoshino .typing import CQEvent
1111from 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
1313import aiohttp
1414
1515# 插件配置
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' )
529610async 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强制检查' )
612694async 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 )
664783async 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 } " )
0 commit comments