diff --git a/src/plugins/nonebot_hk_reporter/platform/monster_siren.py b/src/plugins/nonebot_hk_reporter/platform/monster_siren.py index 7d0044e..3768e33 100644 --- a/src/plugins/nonebot_hk_reporter/platform/monster_siren.py +++ b/src/plugins/nonebot_hk_reporter/platform/monster_siren.py @@ -20,7 +20,7 @@ class MonsterSiren(NewMessage, NoTargetMixin): async def get_target_name(_) -> str: return '塞壬唱片新闻' - async def get_sub_list(self) -> list[RawPost]: + async def get_sub_list(self, _) -> list[RawPost]: async with httpx.AsyncClient() as client: raw_data = await client.get('https://monster-siren.hypergryph.com/api/news') return raw_data.json()['data']['list'] diff --git a/src/plugins/nonebot_hk_reporter/platform/platform.py b/src/plugins/nonebot_hk_reporter/platform/platform.py index e5cd769..b1ecf12 100644 --- a/src/plugins/nonebot_hk_reporter/platform/platform.py +++ b/src/plugins/nonebot_hk_reporter/platform/platform.py @@ -84,7 +84,14 @@ class CategoryMixin(metaclass=RegistryABCMeta, abstract=True): "Return category of given Rawpost" raise NotImplementedError() -class MessageProcessMixin(PlaformNameMixin, CategoryMixin, abstract=True): +class ParsePostMixin(metaclass=RegistryABCMeta, abstract=True): + + @abstractmethod + async def parse(self, raw_post: RawPost) -> Post: + "parse RawPost into post" + ... + +class MessageProcessMixin(PlaformNameMixin, CategoryMixin, ParsePostMixin, abstract=True): "General message process fetch, parse, filter progress" def __init__(self): @@ -95,10 +102,6 @@ class MessageProcessMixin(PlaformNameMixin, CategoryMixin, abstract=True): def get_id(self, post: RawPost) -> Any: "Get post id of given RawPost" - @abstractmethod - async def parse(self, raw_post: RawPost) -> Post: - "parse RawPost into post" - ... async def _parse_with_cache(self, raw_post: RawPost) -> Post: post_id = self.get_id(raw_post) @@ -168,7 +171,7 @@ class NewMessageProcessMixin(StorageMixinProto, MessageProcessMixin, abstract=Tr self.set_stored_data(target, store) return res -class UserCustomFilterMixin(CategoryMixin, abstract=True): +class UserCustomFilterMixin(CategoryMixin, ParsePostMixin, abstract=True): categories: dict[Category, str] enable_tag: bool @@ -196,6 +199,21 @@ class UserCustomFilterMixin(CategoryMixin, abstract=True): res.append(raw_post) return res + async def dispatch_user_post(self, target: Target, new_posts: list[RawPost], users: list[UserSubInfo]) -> list[tuple[User, list[Post]]]: + res: list[tuple[User, list[Post]]] = [] + for user, category_getter, tag_getter in users: + required_tags = tag_getter(target) if self.enable_tag else [] + cats = category_getter(target) + user_raw_post = await self.filter_user_custom(new_posts, cats, required_tags) + user_post: list[Post] = [] + for raw_post in user_raw_post: + if isinstance(self, MessageProcessMixin): + user_post.append(await self._parse_with_cache(raw_post)) + else: + user_post.append(await self.parse(raw_post)) + res.append((user, user_post)) + return res + class Platform(metaclass=RegistryABCMeta, base=True): # schedule_interval: int @@ -220,12 +238,12 @@ class NewMessage( UserCustomFilterMixin, abstract=True ): + "Fetch a list of messages, filter the new messages, dispatch it to different users" async def fetch_new_post(self, target: Target, users: list[UserSubInfo]) -> list[tuple[User, list[Post]]]: try: post_list = await self.get_sub_list(target) new_posts = await self.filter_common_with_diff(target, post_list) - res: list[tuple[User, list[Post]]] = [] if not new_posts: return [] else: @@ -234,17 +252,44 @@ class NewMessage( self.platform_name, target if self.has_target else '-', self.get_id(post))) - for user, category_getter, tag_getter in users: - required_tags = tag_getter(target) if self.enable_tag else [] - cats = category_getter(target) - user_raw_post = await self.filter_user_custom(new_posts, cats, required_tags) - user_post: list[Post] = [] - for raw_post in user_raw_post: - user_post.append(await self._parse_with_cache(raw_post)) - res.append((user, user_post)) + res = await self.dispatch_user_post(target, new_posts, users) self.parse_cache = {} return res except httpx.RequestError as err: logger.warning("network connection error: {}, url: {}".format(type(err), err.request.url)) return [] +class StatusChange( + Platform, + StorageMixinProto, + PlaformNameMixin, + UserCustomFilterMixin, + abstract=True + ): + "Watch a status, and fire a post when status changes" + + @abstractmethod + async def get_status(self, target: Target) -> Any: + ... + + @abstractmethod + def compare_status(self, target: Target, old_status, new_status) -> Optional[RawPost]: + ... + + @abstractmethod + async def parse(self, raw_post: RawPost) -> Post: + ... + + async def fetch_new_post(self, target: Target, users: list[UserSubInfo]) -> list[tuple[User, list[Post]]]: + try: + new_status = await self.get_status(target) + res = [] + if old_status := self.get_stored_data(target): + diff = self.compare_status(target, old_status, new_status) + if diff: + res = await self.dispatch_user_post(target, [diff], users) + self.set_stored_data(target, new_status) + return res + except httpx.RequestError as err: + logger.warning("network connection error: {}, url: {}".format(type(err), err.request.url)) + return [] diff --git a/tests/platforms/monster-siren_list_0.json b/tests/platforms/monster-siren_list_0.json new file mode 100644 index 0000000..b0c40be --- /dev/null +++ b/tests/platforms/monster-siren_list_0.json @@ -0,0 +1,63 @@ +{ + "code": 0, + "msg": "", + "data": { + "list": [ + { + "cid": "114091", + "title": "#AUS小屋", + "cate": 8, + "date": "2021-06-23" + }, + { + "cid": "027726", + "title": "「音律联觉原声EP」正式上架", + "cate": 1, + "date": "2021-06-12" + }, + { + "cid": "750459", + "title": "「ManiFesto:」MV正式公开", + "cate": 1, + "date": "2021-06-08" + }, + { + "cid": "241304", + "title": "「Real Me」正式上架", + "cate": 1, + "date": "2021-06-01" + }, + { + "cid": "578835", + "title": "#D.D.D.PHOTO", + "cate": 8, + "date": "2021-05-24" + }, + { + "cid": "489188", + "title": "#AUS小屋", + "cate": 8, + "date": "2021-05-19" + }, + { + "cid": "992677", + "title": "「Immutable」正式上架", + "cate": 1, + "date": "2021-05-02" + }, + { + "cid": "605962", + "title": "「Voices」正式上架", + "cate": 1, + "date": "2021-05-01" + }, + { + "cid": "336213", + "title": "#D.D.D.PHOTO", + "cate": 8, + "date": "2021-04-28" + } + ], + "end": false + } +} diff --git a/tests/platforms/monster-siren_list_1.json b/tests/platforms/monster-siren_list_1.json new file mode 100644 index 0000000..9f65976 --- /dev/null +++ b/tests/platforms/monster-siren_list_1.json @@ -0,0 +1 @@ +{"code":0,"msg":"","data":{"list":[{"cid":"241303","title":"#D.D.D.PHOTO","cate":8,"date":"2021-06-29"},{"cid":"114091","title":"#AUS小屋","cate":8,"date":"2021-06-23"},{"cid":"027726","title":"「音律联觉原声EP」正式上架","cate":1,"date":"2021-06-12"},{"cid":"750459","title":"「ManiFesto:」MV正式公开","cate":1,"date":"2021-06-08"},{"cid":"241304","title":"「Real Me」正式上架","cate":1,"date":"2021-06-01"},{"cid":"578835","title":"#D.D.D.PHOTO","cate":8,"date":"2021-05-24"},{"cid":"489188","title":"#AUS小屋","cate":8,"date":"2021-05-19"},{"cid":"992677","title":"「Immutable」正式上架","cate":1,"date":"2021-05-02"},{"cid":"605962","title":"「Voices」正式上架","cate":1,"date":"2021-05-01"},{"cid":"336213","title":"#D.D.D.PHOTO","cate":8,"date":"2021-04-28"}],"end":false}} \ No newline at end of file diff --git a/tests/platforms/test_monster-siren.py b/tests/platforms/test_monster-siren.py new file mode 100644 index 0000000..832271f --- /dev/null +++ b/tests/platforms/test_monster-siren.py @@ -0,0 +1,44 @@ +import pytest +import typing +import respx +from httpx import Response +import feedparser + +if typing.TYPE_CHECKING: + import sys + sys.path.append('./src/plugins') + import nonebot_hk_reporter + +from .utils import get_json, get_file + +@pytest.fixture +def monster_siren(plugin_module: 'nonebot_hk_reporter'): + return plugin_module.platform.platform_manager['monster-siren'] + +@pytest.fixture(scope='module') +def monster_siren_list_0(): + return get_json('monster-siren_list_0.json') + +@pytest.fixture(scope='module') +def monster_siren_list_1(): + return get_json('monster-siren_list_1.json') + +@pytest.mark.asyncio +@respx.mock +async def test_fetch_new(monster_siren, dummy_user_subinfo, monster_siren_list_0, monster_siren_list_1): + ak_list_router = respx.get("https://monster-siren.hypergryph.com/api/news") + ak_list_router.mock(return_value=Response(200, json=monster_siren_list_0)) + target = '' + res = await monster_siren.fetch_new_post(target, [dummy_user_subinfo]) + assert(ak_list_router.called) + assert(len(res) == 0) + mock_data = monster_siren_list_1 + ak_list_router.mock(return_value=Response(200, json=mock_data)) + res3 = await monster_siren.fetch_new_post(target, [dummy_user_subinfo]) + assert(len(res3[0][1]) == 1) + post = res3[0][1][0] + assert(post.target_type == 'monster-siren') + assert(post.text == '#D.D.D.PHOTO') + assert(post.url == 'https://monster-siren.hypergryph.com/info/241303') + assert(post.target_name == '塞壬唱片新闻') + assert(len(post.pics) == 0) diff --git a/tests/platforms/test_platform.py b/tests/platforms/test_platform.py index 086ed6e..c6c42b0 100644 --- a/tests/platforms/test_platform.py +++ b/tests/platforms/test_platform.py @@ -1,6 +1,6 @@ import sys import typing -from typing import Any +from typing import Any, Optional import pytest @@ -172,6 +172,52 @@ def mock_platform_no_target(plugin_module: 'nonebot_hk_reporter'): return MockPlatform() +@pytest.fixture +def mock_status_change(plugin_module: 'nonebot_hk_reporter'): + class MockPlatform(plugin_module.platform.platform.StatusChange, + plugin_module.platform.platform.NoTargetMixin): + + platform_name = 'mock_platform' + name = 'Mock Platform' + enabled = True + is_common = True + enable_tag = False + schedule_type = 'interval' + schedule_kw = {'seconds': 10} + categories = { + 1: '转发', + 2: '视频', + } + def __init__(self): + self.sub_index = 0 + super().__init__() + + async def get_status(self, _: 'Target'): + if self.sub_index == 0: + self.sub_index += 1 + return {'s': False} + elif self.sub_index == 1: + self.sub_index += 1 + return {'s': True} + else: + return {'s': False} + + def compare_status(self, target, old_status, new_status) -> Optional['RawPost']: + if old_status['s'] == False and new_status['s'] == True: + return {'text': 'on', 'cat': 1} + elif old_status['s'] == True and new_status['s'] == False: + return {'text': 'off', 'cat': 2} + return None + + async def parse(self, raw_post) -> 'Post': + return plugin_module.post.Post('mock_status', raw_post['text'], '') + + def get_category(self, raw_post): + return raw_post['cat'] + + return MockPlatform() + + @pytest.mark.asyncio async def test_new_message_target_without_cats_tags(mock_platform_without_cats_tags, user_info_factory): res1 = await mock_platform_without_cats_tags.fetch_new_post('dummy', [user_info_factory(lambda _: [1,2], lambda _: [])]) @@ -230,3 +276,25 @@ async def test_new_message_no_target(mock_platform_no_target, user_info_factory) assert('p2' in id_set_1 and 'p3' in id_set_1) assert('p2' in id_set_2) assert('p2' in id_set_3) + +@pytest.mark.asyncio +async def test_status_change(mock_status_change, user_info_factory): + res1 = await mock_status_change.fetch_new_post('dummy', [user_info_factory(lambda _: [1,2], lambda _: [])]) + assert(len(res1) == 0) + res2 = await mock_status_change.fetch_new_post('dummy', [ + user_info_factory(lambda _: [1,2], lambda _:[]) + ]) + assert(len(res2) == 1) + posts = res2[0][1] + assert(len(posts) == 1) + assert(posts[0].text == 'on') + res3 = await mock_status_change.fetch_new_post('dummy', [ + user_info_factory(lambda _: [1,2], lambda _: []), + user_info_factory(lambda _: [1], lambda _: []), + ]) + assert(len(res3) == 2) + assert(len(res3[0][1]) == 1) + assert(res3[0][1][0].text == 'off') + assert(len(res3[1][1]) == 0) + res4 = await mock_status_change.fetch_new_post('dummy', [user_info_factory(lambda _: [1,2], lambda _: [])]) + assert(len(res4) == 0)