diff --git a/CHANGELOG.md b/CHANGELOG.md index 76405d8..1182ba7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## 最近更新 +- feat (issue #67 ):添加屏蔽特定tag的功能 [@AzideCupric](https://github.com/AzideCupric) ([#101](https://github.com/felinae98/nonebot-bison/pull/101)) + ### 新功能 - 在StatusChange中提供了如果api返回错误不更新status的方法 [@felinae98](https://github.com/felinae98) ([#96](https://github.com/felinae98/nonebot-bison/pull/96)) diff --git a/docs/usage/README.md b/docs/usage/README.md index 39bf8e6..b922589 100644 --- a/docs/usage/README.md +++ b/docs/usage/README.md @@ -215,3 +215,29 @@ RSS 链接即为 uid 在网易云网页上电台的链接一般为`https://music.163.com/#/djradio?id=793745436`,`id=` 后面的数字即为 uid + +### 平台订阅标签(Tag) + +社交平台中的 Tag 一般指使用井号(`#`)作为前缀,将关键词进行标记,方便用户进行搜索的功能。 +例子:`#明日方舟# #每日打卡#(weibo、bilibili) #baracamp(Twitter)` + +在 Bison 中,用户在添加平台账号订阅时(如果该平台提供有 hashtag 功能), +会进行`输入需要订阅/屏蔽tag`的步骤。 + +如果需要订阅某些 tag,需要直接向 bison 发送需要订阅的一系列 tag,并使用空格进行分隔。 +例:`A1行动预备组 罗德厨房——回甘` + +如果需要屏蔽某些 tag,需要在需要屏蔽的 tag 前加上前缀`~`,对于复数的 tag,使用空格进行分隔。 +例:`~123罗德岛 ~莱茵生命漫画` + +可以综合运用以上规则进行同时订阅/屏蔽。 +例:`A1行动预备组 ~123罗德岛 罗德厨房——回甘 ~莱茵生命漫画` + +#### Tag 的推送规则 + +每当 Bison 抓取到一条推送,推送中的 Tag 会经过一下检查: + +- Bison 会对**需屏蔽 Tag**进行最优先检查,只要检测到本次推送中存在**任一**已记录的**需屏蔽 tag**,Bison 便会将该推送丢弃。 +- 上一个检查通过后,Bison 会对**需订阅 tag**进行检查,如果本次推送中存在**任一**已记录的**需订阅 tag**,Bison 便会将该推送发送到群中。 +- 当已记录的**需订阅 tag**为空时,只要通过了*第一条规则*的检查,Bison 就会将该推送发送到群中。 +- 当已记录的**需订阅 tag**不为空时,即使通过了*第一条规则*的检查,若本次推送不含**任何**已记录的**需订阅 tag**,Bison 也会将该推送丢弃。 diff --git a/src/plugins/nonebot_bison/config_manager.py b/src/plugins/nonebot_bison/config_manager.py index cc77f2e..a3b71c2 100644 --- a/src/plugins/nonebot_bison/config_manager.py +++ b/src/plugins/nonebot_bison/config_manager.py @@ -137,7 +137,7 @@ def do_add_sub(add_sub: Type[Matcher]): state["id"] = target state["name"] = name except (LookupError): - url = "https://nonebot-bison.vercel.app/usage/#%E6%89%80%E6%94%AF%E6%8C%81%E5%B9%B3%E5%8F%B0%E7%9A%84-uid" + url = "https://nonebot-bison.netlify.app/usage/#%E6%89%80%E6%94%AF%E6%8C%81%E5%B9%B3%E5%8F%B0%E7%9A%84-uid" title = "Bison所支持的平台UID" content = "查询相关平台的uid格式或获取方式" image = "https://s3.bmp.ovh/imgs/2022/03/ab3cc45d83bd3dd3.jpg" @@ -182,13 +182,17 @@ def do_add_sub(add_sub: Type[Matcher]): if not platform_manager[state["platform"]].enable_tag: state["tags"] = [] return - state["_prompt"] = '请输入要订阅的tag,订阅所有tag输入"全部标签"' + state["_prompt"] = '请输入要订阅/屏蔽的tag(不含#号)\n多个tag请使用空格隔开\n具体规则回复"详情"' async def parser_tags(event: MessageEvent, state: T_State): if not isinstance(state["tags"], Message): return if str(event.get_message()).strip() == "取消": # 一般不会有叫 取消 的tag吧 await add_sub.finish("已中止订阅") + if str(event.get_message()).strip() == "详情": + await add_sub.reject( + '订阅tag直接输入tag内容\n订阅所有tag输入"全部标签"\n屏蔽tag请在tag名称前添加~号\n详见https://nonebot-bison.netlify.app/usage/#平台订阅标签-tag' + ) if str(event.get_message()).strip() == "全部标签": state["tags"] = [] else: diff --git a/src/plugins/nonebot_bison/platform/platform.py b/src/plugins/nonebot_bison/platform/platform.py index 56ae4d6..5b694aa 100644 --- a/src/plugins/nonebot_bison/platform/platform.py +++ b/src/plugins/nonebot_bison/platform/platform.py @@ -110,6 +110,43 @@ class Platform(metaclass=RegistryABCMeta, base=True): def set_stored_data(self, target: Target, data: Any): self.store[target] = data + def tag_separator(self, stored_tags: list[Tag]) -> tuple[list[Tag], list[Tag]]: + """返回分离好的正反tag元组""" + subscribed_tags = [] + banned_tags = [] + for tag in stored_tags: + if tag.startswith("~"): + banned_tags.append(tag.lstrip("~")) + else: + subscribed_tags.append(tag) + return subscribed_tags, banned_tags + + def is_banned_post( + self, + post_tags: Collection[Tag], + subscribed_tags: list[Tag], + banned_tags: list[Tag], + ) -> bool: + """只要存在任意屏蔽tag则返回真,此行为优先级最高。 + 存在任意被订阅tag则返回假,此行为优先级次之。 + 若被订阅tag为空,则返回假。 + """ + # 存在任意需要屏蔽的tag则为真 + if banned_tags: + for tag in post_tags or []: + if tag in banned_tags: + return True + # 检测屏蔽tag后,再检测订阅tag + # 存在任意需要订阅的tag则为假 + if subscribed_tags: + ban_it = True + for tag in post_tags or []: + if tag in subscribed_tags: + ban_it = False + return ban_it + else: + return False + async def filter_user_custom( self, raw_post_list: list[RawPost], cats: list[Category], tags: list[Tag] ) -> list[RawPost]: @@ -120,13 +157,9 @@ class Platform(metaclass=RegistryABCMeta, base=True): if cats and cat not in cats: continue if self.enable_tag and tags: - flag = False - post_tags = self.get_tags(raw_post) - for tag in post_tags or []: - if tag in tags: - flag = True - break - if not flag: + if self.is_banned_post( + self.get_tags(raw_post), *self.tag_separator(tags) + ): continue res.append(raw_post) return res diff --git a/tests/platforms/static/bilibili_fake_dy_list.json b/tests/platforms/static/bilibili_fake_dy_list.json new file mode 100644 index 0000000..1890764 --- /dev/null +++ b/tests/platforms/static/bilibili_fake_dy_list.json @@ -0,0 +1,246 @@ +{ + "data": { + "cards": [ + { + "desc": { + "type": 2 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "" + } + ] + } + } + }, + { + "desc": { + "type": 1 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "" + } + ] + } + } + }, + { + "desc": { + "type": 8 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "明日方舟" + }, + { + "topic_name": "风笛" + }, + { + "topic_name": "琴柳" + }, + { + "topic_name": "风暴瞭望" + }, + { + "topic_name": "轮换池" + }, + { + "topic_name": "打卡挑战" + } + ] + } + } + }, + { + "desc": { + "type": 2 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "" + } + ] + } + } + }, + { + "desc": { + "type": 1 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "明日方舟" + }, + { + "topic_name": "饼学大厦" + }, + { + "topic_name": "可露希尔的秘密档案" + } + ] + } + } + }, + { + "desc": { + "type": 1 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "罗德岛相簿" + }, + { + "topic_name": "可露希尔的秘密档案" + }, + { + "topic_name": "罗德岛闲逛部" + } + ] + } + } + }, + { + "desc": { + "type": 8 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "明日方舟" + }, + { + "topic_name": "轮换学" + }, + { + "topic_name": "常驻标准寻访" + }, + { + "topic_name": "轮换池" + }, + { + "topic_name": "打卡挑战" + }, + { + "topic_name": "舟游" + } + ] + } + } + }, + { + "desc": { + "type": 2 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "明日方舟" + }, + { + "topic_name": "饼学大厦" + } + ] + } + } + }, + { + "desc": { + "type": 4 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "" + } + ] + } + } + }, + { + "desc": { + "type": 1 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "明日方舟" + }, + { + "topic_name": "饼学大厦" + }, + { + "topic_name": "罗德岛相簿" + }, + { + "topic_name": "可露希尔的秘密档案" + }, + { + "topic_name": "罗德岛闲逛部" + } + ] + } + } + }, + { + "desc": { + "type": 1 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "明日方舟" + }, + { + "topic_name": "饼学大厦" + } + ] + } + } + }, + { + "desc": { + "type": 1 + }, + "display": { + "topic_info": { + "topic_details": [ + { + "topic_name": "明日方舟" + }, + { + "topic_name": "饼学大厦" + }, + { + "topic_name": "罗德岛相簿" + }, + { + "topic_name": "可露希尔的秘密档案" + }, + { + "topic_name": "罗德岛闲逛部" + } + ] + } + } + } + ] + } +} \ No newline at end of file diff --git a/tests/platforms/static/tag_cases.json b/tests/platforms/static/tag_cases.json new file mode 100644 index 0000000..01c9af9 --- /dev/null +++ b/tests/platforms/static/tag_cases.json @@ -0,0 +1,129 @@ +[ + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": [ + "222" + ], + "banned_tags": [ + "555" + ] + }, + "result": false + }, + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": [], + "banned_tags": [ + "555" + ] + }, + "result": false + }, + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": [], + "banned_tags": [ + "444" + ] + }, + "result": true + }, + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": [ + "222" + ], + "banned_tags": [] + }, + "result": false + }, + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": [], + "banned_tags": [] + }, + "result": false + }, + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": ["111","555","666"], + "banned_tags": [] + }, + "result": false + }, + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": ["111","555"], + "banned_tags": ["333"] + }, + "result": true + }, + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": ["111","333"], + "banned_tags": ["111"] + }, + "result": true + }, + { + "case": { + "post_tags": [ + "111", + "222", + "333", + "444" + ], + "subscribed_tags": ["222"], + "banned_tags": ["555","333"] + }, + "result": true + } +] \ No newline at end of file diff --git a/tests/platforms/test_bilibili.py b/tests/platforms/test_bilibili.py index f331d2d..a4629e3 100644 --- a/tests/platforms/test_bilibili.py +++ b/tests/platforms/test_bilibili.py @@ -98,3 +98,33 @@ async def test_parse_target(bilibili: "Bilibili"): await bilibili.parse_target( "https://www.bilibili.com/video/BV1qP4y1g738?spm_id_from=333.999.0.0" ) + + +@pytest.fixture(scope="module") +def post_list(): + return get_json("bilibili_fake_dy_list.json")["data"]["cards"] + + +# 测试新tag机制的平台推送情况 +@pytest.mark.asyncio +async def test_filter_user_custom(bilibili, post_list): + + only_banned_tags = ["~可露希尔的秘密档案"] + res0 = await bilibili.filter_user_custom(post_list, [], only_banned_tags) + assert len(res0) == 8 + + only_subscribed_tags = ["可露希尔的秘密档案"] + res1 = await bilibili.filter_user_custom(post_list, [], only_subscribed_tags) + assert len(res1) == 4 + + multi_subs_tags_1 = ["可露希尔的秘密档案", "罗德岛相簿"] + res2 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_1) + assert len(res2) == 4 + + multi_subs_tags_2 = ["罗德岛相簿", "风暴瞭望"] + res3 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_2) + assert len(res3) == 4 + + multi_subs_tags_3 = ["明日方舟", "~饼学大厦"] + res4 = await bilibili.filter_user_custom(post_list, [], multi_subs_tags_3) + assert len(res4) == 2 diff --git a/tests/platforms/test_platform_tag_filter.py b/tests/platforms/test_platform_tag_filter.py new file mode 100644 index 0000000..c9d399a --- /dev/null +++ b/tests/platforms/test_platform_tag_filter.py @@ -0,0 +1,32 @@ +import pytest +from nonebug.app import App + +from .utils import get_json + + +@pytest.fixture(scope="module") +def test_cases(): + return get_json("tag_cases.json") + + +# 测试正反tag的判断情况 +@pytest.mark.asyncio +async def test_filter_user_custom_tag(app: App, test_cases): + from nonebot_bison.platform import platform_manager + + bilibili = platform_manager["bilibili"] + for case in test_cases: + res = bilibili.is_banned_post(**case["case"]) + assert res == case["result"] + + +# 测试正反tag的分离情况 +@pytest.mark.asyncio +async def test_tag_separator(app: App): + from nonebot_bison.platform import platform_manager + + bilibili = platform_manager["bilibili"] + tags = ["~111", "222", "333", "~444", "555"] + res = bilibili.tag_separator(tags) + assert res[0] == ["222", "333", "555"] + assert res[1] == ["111", "444"] diff --git a/tests/test_config_manager_add.py b/tests/test_config_manager_add.py index b75385b..4dc2b1f 100644 --- a/tests/test_config_manager_add.py +++ b/tests/test_config_manager_add.py @@ -1,3 +1,5 @@ +from email import message + import pytest import respx from httpx import Response @@ -151,12 +153,20 @@ async def test_add_with_target(app: App, init_scheduler): ) ctx.receive_event(bot, event_5_ok) ctx.should_call_send(event_5_ok, Message(BotReply.add_reply_on_tags), True) - event_6 = fake_group_message_event( + event_6_more_info = fake_group_message_event( + message=Message("详情"), sender=fake_admin_user + ) + ctx.receive_event(bot, event_6_more_info) + ctx.should_call_send( + event_6_more_info, BotReply.add_reply_on_tags_need_more_info, True + ) + ctx.should_rejected() + event_6_ok = fake_group_message_event( message=Message("全部标签"), sender=fake_admin_user ) - ctx.receive_event(bot, event_6) + ctx.receive_event(bot, event_6_ok) ctx.should_call_send( - event_6, BotReply.add_reply_subscribe_success("明日方舟Arknights"), True + event_6_ok, BotReply.add_reply_subscribe_success("明日方舟Arknights"), True ) ctx.should_finished() subs = await config.list_subscribe(10000, "group") diff --git a/tests/utils.py b/tests/utils.py index 08bdbe2..003ed07 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -99,7 +99,7 @@ class BotReply: @staticmethod def add_reply_on_id_input_search(): - search_url = "https://nonebot-bison.vercel.app/usage/#%E6%89%80%E6%94%AF%E6%8C%81%E5%B9%B3%E5%8F%B0%E7%9A%84-uid" + search_url = "https://nonebot-bison.netlify.app/usage/#%E6%89%80%E6%94%AF%E6%8C%81%E5%B9%B3%E5%8F%B0%E7%9A%84-uid" search_title = "Bison所支持的平台UID" search_content = "查询相关平台的uid格式或获取方式" search_image = "https://s3.bmp.ovh/imgs/2022/03/ab3cc45d83bd3dd3.jpg" @@ -144,5 +144,6 @@ class BotReply: add_reply_on_id_input_error = "id输入错误" add_reply_on_target_parse_input_error = "不能从你的输入中提取出id,请检查你输入的内容是否符合预期" add_reply_on_platform_input_error = "平台输入错误" - add_reply_on_tags = '请输入要订阅的tag,订阅所有tag输入"全部标签"' + add_reply_on_tags = '请输入要订阅/屏蔽的tag(不含#号)\n多个tag请使用空格隔开\n具体规则回复"详情"' + add_reply_on_tags_need_more_info = '订阅tag直接输入tag内容\n订阅所有tag输入"全部标签"\n屏蔽tag请在tag名称前添加~号\n详见https://nonebot-bison.netlify.app/usage/#平台订阅标签-tag' add_reply_abort = "已中止订阅"