From 9e6a9228f61d1dedcd4153b0d0d54ff2550baec3 Mon Sep 17 00:00:00 2001
From: felinae98 <731499577@qq.com>
Date: Tue, 29 Jun 2021 19:43:06 +0800
Subject: [PATCH] Add status chage and test

---
 .../platform/monster_siren.py                 |  2 +-
 .../nonebot_hk_reporter/platform/platform.py  | 75 +++++++++++++++----
 tests/platforms/monster-siren_list_0.json     | 63 ++++++++++++++++
 tests/platforms/monster-siren_list_1.json     |  1 +
 tests/platforms/test_monster-siren.py         | 44 +++++++++++
 tests/platforms/test_platform.py              | 70 ++++++++++++++++-
 6 files changed, 238 insertions(+), 17 deletions(-)
 create mode 100644 tests/platforms/monster-siren_list_0.json
 create mode 100644 tests/platforms/monster-siren_list_1.json
 create mode 100644 tests/platforms/test_monster-siren.py

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)