From 9bf8745cf70138c8a79b7eaff5e5b252f8548e62 Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Wed, 9 Feb 2022 19:46:04 +0800 Subject: [PATCH 01/10] try fix #22 --- src/plugins/nonebot_bison/utils.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/plugins/nonebot_bison/utils.py b/src/plugins/nonebot_bison/utils.py index f7e459c..073c62e 100644 --- a/src/plugins/nonebot_bison/utils.py +++ b/src/plugins/nonebot_bison/utils.py @@ -2,6 +2,7 @@ import asyncio import base64 from html import escape import os +import platform import re import subprocess import sys @@ -15,6 +16,8 @@ from nonebot.log import logger from nonebot.log import default_format from playwright._impl._driver import compute_driver_executable from playwright.async_api import Browser, Page, Playwright, async_playwright +from uvicorn.loops import asyncio as _asyncio +from uvicorn import config from .plugin_config import plugin_config @@ -184,3 +187,17 @@ if plugin_config.bison_filter_log: default_filter.level = ( "DEBUG" if config.debug else "INFO") if config.log_level is None else config.log_level + +# monkey patch +def asyncio_setup(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + +@property +def should_reload(self): + return False + +if platform.system() == "Windows": + _asyncio.asyncio_setup = asyncio_setup + config.Config.should_reload = should_reload # type:ignore + logger.warning('检测到当前为 Windows 系统,已自动注入猴子补丁') From 69a4f59c04817c148f90968578235789eb3c65d3 Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Thu, 10 Feb 2022 16:16:22 +0800 Subject: [PATCH 02/10] disable update browser --- .pre-commit-config.yaml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b577ac8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +ci: + autofix_commit_msg: "auto fix by pre-commit hooks" + autofix_prs: true + autoupdate_branch: dev + autoupdate_schedule: weekly + autoupdate_commit_msg: "auto update by pre-commit hooks" +repos: + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort + + - repo: https://github.com/psf/black + rev: 22.1.0 + hooks: + - id: black + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.5.1 + hooks: + - id: prettier + types_or: [markdown] From 8e3de419d37cd24a8988b11846eec19e0a47b760 Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Thu, 10 Feb 2022 16:17:19 +0800 Subject: [PATCH 03/10] disable update browser --- src/plugins/nonebot_bison/utils.py | 171 ++++++++++++++++++----------- 1 file changed, 109 insertions(+), 62 deletions(-) diff --git a/src/plugins/nonebot_bison/utils.py b/src/plugins/nonebot_bison/utils.py index 073c62e..1570f5e 100644 --- a/src/plugins/nonebot_bison/utils.py +++ b/src/plugins/nonebot_bison/utils.py @@ -1,36 +1,48 @@ import asyncio import base64 -from html import escape import os import platform import re import subprocess import sys +from html import escape +from pathlib import Path from time import asctime from typing import Awaitable, Callable, Optional, Union -from bs4 import BeautifulSoup as bs import nonebot +from bs4 import BeautifulSoup as bs from nonebot.adapters.cqhttp.message import MessageSegment -from nonebot.log import logger -from nonebot.log import default_format +from nonebot.log import default_format, logger from playwright._impl._driver import compute_driver_executable from playwright.async_api import Browser, Page, Playwright, async_playwright -from uvicorn.loops import asyncio as _asyncio from uvicorn import config +from uvicorn.loops import asyncio as _asyncio from .plugin_config import plugin_config + class Singleton(type): _instances = {} + def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) return cls._instances[cls] + @nonebot.get_driver().on_startup def download_browser(): if not plugin_config.bison_browser and not plugin_config.bison_use_local: + system = platform.system() + if system == "Linux": + browser_path = Path.home() / ".cache" / "ms-playwright" + elif system == "Windows": + browser_path = Path.home() / "AppData" / "Local" / "ms-playwright" + else: + raise RuntimeError("platform not supported") + if browser_path.exists() and os.listdir(str(browser_path)): + logger.warning("Browser Exists, skip") env = os.environ.copy() driver_executable = compute_driver_executable() env["PW_CLI_TARGET_LANG"] = "python" @@ -38,65 +50,84 @@ def download_browser(): class Render(metaclass=Singleton): - def __init__(self): self.lock = asyncio.Lock() self.browser: Browser - self.interval_log = '' + self.interval_log = "" self.remote_browser = False async def get_browser(self, playwright: Playwright) -> Browser: if plugin_config.bison_browser: - if plugin_config.bison_browser.startswith('local:'): - path = plugin_config.bison_browser.split('local:', 1)[1] + if plugin_config.bison_browser.startswith("local:"): + path = plugin_config.bison_browser.split("local:", 1)[1] return await playwright.chromium.launch( - executable_path=path, args=['--no-sandbox']) - if plugin_config.bison_browser.startswith('ws:'): + executable_path=path, args=["--no-sandbox"] + ) + if plugin_config.bison_browser.startswith("ws:"): self.remote_browser = True return await playwright.chromium.connect(plugin_config.bison_browser) - if plugin_config.bison_browser.startswith('wsc:'): + if plugin_config.bison_browser.startswith("wsc:"): self.remote_browser = True return await playwright.chromium.connect_over_cdp( - 'ws:' + plugin_config.bison_browser[4:] - ) - raise RuntimeError('bison_BROWSER error') + "ws:" + plugin_config.bison_browser[4:] + ) + raise RuntimeError("bison_BROWSER error") if plugin_config.bison_use_local: return await playwright.chromium.launch( - executable_path='/usr/bin/chromium', args=['--no-sandbox']) - return await playwright.chromium.launch(args=['--no-sandbox']) + executable_path="/usr/bin/chromium", args=["--no-sandbox"] + ) + return await playwright.chromium.launch(args=["--no-sandbox"]) async def close_browser(self): if not self.remote_browser: await self.browser.close() - async def render(self, url: str, viewport: Optional[dict] = None, target: Optional[str] = None, - operation: Optional[Callable[[Page], Awaitable[None]]] = None) -> Optional[bytes]: + async def render( + self, + url: str, + viewport: Optional[dict] = None, + target: Optional[str] = None, + operation: Optional[Callable[[Page], Awaitable[None]]] = None, + ) -> Optional[bytes]: retry_times = 0 - self.interval_log = '' + self.interval_log = "" while retry_times < 3: try: - return await asyncio.wait_for(self.do_render(url, viewport, target, operation), 20) + return await asyncio.wait_for( + self.do_render(url, viewport, target, operation), 20 + ) except asyncio.TimeoutError: retry_times += 1 - logger.warning("render error {}\n".format(retry_times) + self.interval_log) - self.interval_log = '' + logger.warning( + "render error {}\n".format(retry_times) + self.interval_log + ) + self.interval_log = "" # if self.browser: # await self.browser.close() # self.lock.release() def _inter_log(self, message: str) -> None: - self.interval_log += asctime() + '' + message + '\n' + self.interval_log += asctime() + "" + message + "\n" - async def do_render(self, url: str, viewport: Optional[dict] = None, target: Optional[str] = None, - operation: Optional[Callable[[Page], Awaitable[None]]] = None) -> Optional[bytes]: + async def do_render( + self, + url: str, + viewport: Optional[dict] = None, + target: Optional[str] = None, + operation: Optional[Callable[[Page], Awaitable[None]]] = None, + ) -> Optional[bytes]: async with self.lock: async with async_playwright() as playwright: self.browser = await self.get_browser(playwright) - self._inter_log('open browser') + self._inter_log("open browser") if viewport: constext = await self.browser.new_context( - viewport={'width': viewport['width'], 'height': viewport['height']}, - device_scale_factor=viewport.get('deviceScaleFactor', 1)) + viewport={ + "width": viewport["width"], + "height": viewport["height"], + }, + device_scale_factor=viewport.get("deviceScaleFactor", 1), + ) page = await constext.new_page() else: page = await self.browser.new_page() @@ -104,51 +135,57 @@ class Render(metaclass=Singleton): await operation(page) else: await page.goto(url) - self._inter_log('open page') + self._inter_log("open page") if target: target_ele = page.locator(target) if not target_ele: return None - data = await target_ele.screenshot(type='jpeg') + data = await target_ele.screenshot(type="jpeg") else: - data = await page.screenshot(type='jpeg') - self._inter_log('screenshot') + data = await page.screenshot(type="jpeg") + self._inter_log("screenshot") await page.close() - self._inter_log('close page') + self._inter_log("close page") await self.close_browser() - self._inter_log('close browser') - assert(isinstance(data, bytes)) + self._inter_log("close browser") + assert isinstance(data, bytes) return data async def text_to_pic(self, text: str) -> Optional[bytes]: - lines = text.split('\n') - parsed_lines = list(map(lambda x: '

{}

'.format(escape(x)), lines)) - html_text = '
{}
'.format(''.join(parsed_lines)) - url = 'data:text/html;charset=UTF-8;base64,{}'.format(base64.b64encode(html_text.encode()).decode()) - data = await self.render(url, target='div') + lines = text.split("\n") + parsed_lines = list(map(lambda x: "

{}

".format(escape(x)), lines)) + html_text = '
{}
'.format( + "".join(parsed_lines) + ) + url = "data:text/html;charset=UTF-8;base64,{}".format( + base64.b64encode(html_text.encode()).decode() + ) + data = await self.render(url, target="div") return data - async def text_to_pic_cqcode(self, text:str) -> MessageSegment: + async def text_to_pic_cqcode(self, text: str) -> MessageSegment: data = await self.text_to_pic(text) # logger.debug('file size: {}'.format(len(data))) if data: # logger.debug(code) return MessageSegment.image(data) else: - return MessageSegment.text('生成图片错误') + return MessageSegment.text("生成图片错误") + async def parse_text(text: str) -> MessageSegment: - 'return raw text if don\'t use pic, otherwise return rendered opcode' + "return raw text if don't use pic, otherwise return rendered opcode" if plugin_config.bison_use_pic: render = Render() return await render.text_to_pic_cqcode(text) else: return MessageSegment.text(text) + def html_to_text(html: str, query_dict: dict = {}) -> str: - html = re.sub(r'', '
\n', html) - html = html.replace('

', '

\n') - soup = bs(html, 'html.parser') + html = re.sub(r"", "
\n", html) + html = html.replace("

", "

\n") + soup = bs(html, "html.parser") if query_dict: node = soup.find(**query_dict) else: @@ -158,7 +195,6 @@ def html_to_text(html: str, query_dict: dict = {}) -> str: class Filter: - def __init__(self) -> None: self.level: Union[int, str] = "DEBUG" @@ -168,36 +204,47 @@ class Filter: if module: module_name = getattr(module, "__module_name__", module_name) record["name"] = module_name.split(".")[0] - levelno = logger.level(self.level).no if isinstance(self.level, - str) else self.level + levelno = ( + logger.level(self.level).no if isinstance(self.level, str) else self.level + ) nonebot_warning_level = logger.level("WARNING").no - return record["level"].no >= levelno if record["name"] != "nonebot" \ - else record["level"].no >= nonebot_warning_level + return ( + record["level"].no >= levelno + if record["name"] != "nonebot" + else record["level"].no >= nonebot_warning_level + ) + if plugin_config.bison_filter_log: logger.remove() default_filter = Filter() - logger.add(sys.stdout, - colorize=True, - diagnose=False, - filter=default_filter, - format=default_format) + logger.add( + sys.stdout, + colorize=True, + diagnose=False, + filter=default_filter, + format=default_format, + ) config = nonebot.get_driver().config - logger.success("Muted info & success from nonebot") + logger.success("Muted info & success from nonebot") default_filter.level = ( - "DEBUG" if config.debug else - "INFO") if config.log_level is None else config.log_level + ("DEBUG" if config.debug else "INFO") + if config.log_level is None + else config.log_level + ) # monkey patch def asyncio_setup(): loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) + @property def should_reload(self): return False + if platform.system() == "Windows": _asyncio.asyncio_setup = asyncio_setup - config.Config.should_reload = should_reload # type:ignore - logger.warning('检测到当前为 Windows 系统,已自动注入猴子补丁') + config.Config.should_reload = should_reload # type:ignore + logger.warning("检测到当前为 Windows 系统,已自动注入猴子补丁") From 3d43d4703404590fa1c75ea33aca031502582950 Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Thu, 10 Feb 2022 16:26:41 +0800 Subject: [PATCH 04/10] disable update browser. (really!!) --- src/plugins/nonebot_bison/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/nonebot_bison/utils.py b/src/plugins/nonebot_bison/utils.py index 1570f5e..63774d7 100644 --- a/src/plugins/nonebot_bison/utils.py +++ b/src/plugins/nonebot_bison/utils.py @@ -43,6 +43,7 @@ def download_browser(): raise RuntimeError("platform not supported") if browser_path.exists() and os.listdir(str(browser_path)): logger.warning("Browser Exists, skip") + return env = os.environ.copy() driver_executable = compute_driver_executable() env["PW_CLI_TARGET_LANG"] = "python" From a19d8c7b0b18bd83690620f8d32c88fffcee628b Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Thu, 10 Feb 2022 16:35:55 +0800 Subject: [PATCH 05/10] switch dockerfile order --- docker/Dockerfile_with_frontend | 2 +- docker/Dockerfile_with_frontend_sentry | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/Dockerfile_with_frontend b/docker/Dockerfile_with_frontend index 9a98ffc..69ad203 100644 --- a/docker/Dockerfile_with_frontend +++ b/docker/Dockerfile_with_frontend @@ -9,9 +9,9 @@ RUN apt-get update && apt-get install -y xvfb fonts-noto-color-emoji ttf-unifont libnspr4 libnss3 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ libxdamage1 libxext6 libxfixes3 libxrandr2 libxshmfence1 \ && rm -rf /var/lib/apt/lists/* -RUN pip install playwright && playwright install chromium COPY ./pyproject.toml ./poetry.lock* /app/ RUN poetry install --no-root --no-dev +RUN playwright install chromium ADD src /app/src ADD bot.py /app/ ENV HOST=0.0.0.0 diff --git a/docker/Dockerfile_with_frontend_sentry b/docker/Dockerfile_with_frontend_sentry index 146007e..beef9b3 100644 --- a/docker/Dockerfile_with_frontend_sentry +++ b/docker/Dockerfile_with_frontend_sentry @@ -9,11 +9,11 @@ RUN apt-get update && apt-get install -y xvfb fonts-noto-color-emoji ttf-unifont libnspr4 libnss3 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ libxdamage1 libxext6 libxfixes3 libxrandr2 libxshmfence1 \ && rm -rf /var/lib/apt/lists/* -RUN pip install playwright && playwright install chromium COPY ./pyproject.toml ./poetry.lock* ./bot.py /app/ RUN poetry add nonebot-plugin-sentry && \ sed '/nonebot.load_builtin_plugins()/a nonebot.load_plugin("nonebot_plugin_sentry")' -i bot.py RUN poetry install --no-root --no-dev +RUN playwright install chromium ADD src /app/src ENV HOST=0.0.0.0 CMD ["python", "bot.py"] From 6a30f4c24d18f83c534a29849634549b0cf89331 Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Sat, 12 Feb 2022 00:56:44 +0800 Subject: [PATCH 06/10] update to v4.4 --- CHANGELOG.md | 39 +++++++++++++++++++++++++++------------ pyproject.toml | 2 +- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03616fe..c858311 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,44 +1,59 @@ # Change Log ## [0.2.11] - 2021-06-17 + - 增加了简单的单元测试 - 增加了管理员直接管理订阅的能力 ## [0.3.0] - 2021-07-06 -- 微博tag支持 -- 修复bug + +- 微博 tag 支持 +- 修复 bug - 增加微博超话和纯文字支持 - 更改浏览器配置 - 将“来源”移动到文末 -- 使用组合来构建新的platform,新增状态改变类型订阅 +- 使用组合来构建新的 platform,新增状态改变类型订阅 ## [0.3.1] - 2021-07-10 + - 修复不发送来源 -- 发送RSS订阅的title +- 发送 RSS 订阅的 title - 修复浏览器渲染问题 ## [0.3.2] - 2021-09-28 -- 增加NoTargetGroup -- 增加1x3拼图的支持 + +- 增加 NoTargetGroup +- 增加 1x3 拼图的支持 - 增加网易云 ## [0.3.3] - 2021-09-28 + - 修复拼图问题 ## [0.4.0] - 2021-11-18 -- 项目更名为nonebot-bison + +- 项目更名为 nonebot-bison ## [0.4.1] - 2021-11-31 + - 加入了管理后台 ## [0.4.2] + 并没有做什么只是为了修复前端文件没有正确打包的问题开了个新的版本号 -推上pypi +推上 pypi ## [0.4.3] -- 使用playwright替换pypeteer(大概能修复渲染失败图片之后CPU跑满的问题) -- 增加了help插件`nonebot-plugin-help` -- 修复playwright漏内存的问题 -- 增加过滤nonebot日志功能 + +- 使用 playwright 替换 pypeteer(大概能修复渲染失败图片之后 CPU 跑满的问题) +- 增加了 help 插件`nonebot-plugin-help` +- 修复 playwright 漏内存的问题 +- 增加过滤 nonebot 日志功能 - 前端可以刷新了(之前居然不可以) - 在镜像里塞进了浏览器(导致镜像体积起飞) + +## [0.4.4] + +- 又双叒叕重构了一下 +- 修复了 Docker 中 Playwright 下载的浏览器版本不正确问题 +- 加入了猴子补丁,使 Windows 里能运行 Playwright diff --git a/pyproject.toml b/pyproject.toml index a706788..9ae2ecc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "nonebot-bison" -version = "0.4.3" +version = "0.4.4" description = "Subscribe message from social medias" authors = ["felinae98 "] license = "MIT" From 709a3e214b92168a122edd1737c498828ab7979d Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Sat, 12 Feb 2022 10:20:02 +0800 Subject: [PATCH 07/10] format code --- .pre-commit-config.yaml | 1 + bot.py | 6 +- src/plugins/auto_agree.py | 24 +- src/plugins/nonebot_bison/__init__.py | 27 +- .../nonebot_bison/admin_page/__init__.py | 95 ++-- src/plugins/nonebot_bison/admin_page/api.py | 155 ++++--- src/plugins/nonebot_bison/admin_page/jwt.py | 19 +- .../nonebot_bison/admin_page/token_manager.py | 16 +- src/plugins/nonebot_bison/config.py | 157 +++++-- src/plugins/nonebot_bison/config_manager.py | 226 +++++---- .../nonebot_bison/platform/__init__.py | 16 +- .../nonebot_bison/platform/arknights.py | 144 +++--- .../nonebot_bison/platform/bilibili.py | 109 +++-- .../nonebot_bison/platform/ncm_artist.py | 44 +- .../nonebot_bison/platform/ncm_radio.py | 46 +- .../nonebot_bison/platform/platform.py | 154 ++++-- src/plugins/nonebot_bison/platform/rss.py | 27 +- src/plugins/nonebot_bison/platform/wechat.py | 6 +- src/plugins/nonebot_bison/platform/weibo.py | 153 +++--- src/plugins/nonebot_bison/plugin_config.py | 14 +- src/plugins/nonebot_bison/post.py | 69 +-- src/plugins/nonebot_bison/scheduler.py | 71 ++- src/plugins/nonebot_bison/send.py | 19 +- src/plugins/nonebot_bison/types.py | 12 +- tests/conftest.py | 29 +- tests/platforms/test_arknights.py | 93 ++-- tests/platforms/test_bilibili.py | 46 +- tests/platforms/test_ncm_artist.py | 53 ++- tests/platforms/test_ncm_radio.py | 58 +-- tests/platforms/test_platform.py | 437 ++++++++++-------- tests/platforms/test_weibo.py | 133 +++--- tests/platforms/utils.py | 16 +- tests/test_config_manager.py | 59 ++- tests/test_merge_pic.py | 96 ++-- tests/test_render.py | 15 +- 35 files changed, 1613 insertions(+), 1032 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b577ac8..77e9bc6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,6 +9,7 @@ repos: rev: 5.10.1 hooks: - id: isort + args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black rev: 22.1.0 diff --git a/bot.py b/bot.py index 6819d73..957a2e5 100644 --- a/bot.py +++ b/bot.py @@ -5,11 +5,11 @@ nonebot.init(command_start=[""]) app = nonebot.get_asgi() driver = nonebot.get_driver() -driver.register_adapter('cqhttp', CQHTTPBot) +driver.register_adapter("cqhttp", CQHTTPBot) nonebot.load_builtin_plugins() -nonebot.load_plugin('nonebot_plugin_help') -nonebot.load_plugins('src/plugins') +nonebot.load_plugin("nonebot_plugin_help") +nonebot.load_plugins("src/plugins") if __name__ == "__main__": nonebot.run(app="bot:app") diff --git a/src/plugins/auto_agree.py b/src/plugins/auto_agree.py index d4b4998..463e54a 100644 --- a/src/plugins/auto_agree.py +++ b/src/plugins/auto_agree.py @@ -1,18 +1,26 @@ -from nonebot import on_request, logger +from nonebot import logger, on_request from nonebot.adapters.cqhttp import Bot, Event +from nonebot.adapters.cqhttp.event import ( + FriendRequestEvent, + GroupRequestEvent, + RequestEvent, +) +from nonebot.adapters.cqhttp.permission import PRIVATE_FRIEND from nonebot.permission import SUPERUSER from nonebot.typing import T_State -from nonebot.adapters.cqhttp.permission import PRIVATE_FRIEND -from nonebot.adapters.cqhttp.event import FriendRequestEvent, GroupRequestEvent, RequestEvent friend_req = on_request(priority=5) + @friend_req.handle() async def add_superuser(bot: Bot, event: RequestEvent, state: T_State): - if str(event.user_id) in bot.config.superusers and event.request_type == 'private': + if str(event.user_id) in bot.config.superusers and event.request_type == "private": await event.approve(bot) - logger.info('add user {}'.format(event.user_id)) - elif event.sub_type == 'invite' and str(event.user_id) in bot.config.superusers and event.request_type == 'group': + logger.info("add user {}".format(event.user_id)) + elif ( + event.sub_type == "invite" + and str(event.user_id) in bot.config.superusers + and event.request_type == "group" + ): await event.approve(bot) - logger.info('add group {}'.format(event.group_id)) - + logger.info("add group {}".format(event.group_id)) diff --git a/src/plugins/nonebot_bison/__init__.py b/src/plugins/nonebot_bison/__init__.py index 73abbeb..353a8a3 100644 --- a/src/plugins/nonebot_bison/__init__.py +++ b/src/plugins/nonebot_bison/__init__.py @@ -1,16 +1,17 @@ import nonebot -from . import config_manager -from . import config -from . import scheduler -from . import send -from . import post -from . import platform -from . import types -from . import utils -from . import admin_page +from . import ( + admin_page, + config, + config_manager, + platform, + post, + scheduler, + send, + types, + utils, +) -__help__version__ = '0.4.3' -__help__plugin__name__ = 'nonebot_bison' -__usage__ = ('本bot可以提供b站、微博等社交媒体的消息订阅,详情' - '请查看本bot文档,或者at本bot发送“添加订阅”订阅第一个帐号') +__help__version__ = "0.4.3" +__help__plugin__name__ = "nonebot_bison" +__usage__ = "本bot可以提供b站、微博等社交媒体的消息订阅,详情" "请查看本bot文档,或者at本bot发送“添加订阅”订阅第一个帐号" diff --git a/src/plugins/nonebot_bison/admin_page/__init__.py b/src/plugins/nonebot_bison/admin_page/__init__.py index 745e0ce..0aba931 100644 --- a/src/plugins/nonebot_bison/admin_page/__init__.py +++ b/src/plugins/nonebot_bison/admin_page/__init__.py @@ -1,8 +1,9 @@ -from dataclasses import dataclass import os +from dataclasses import dataclass from pathlib import Path from typing import Union +import socketio from fastapi.staticfiles import StaticFiles from nonebot import get_driver, on_command from nonebot.adapters.cqhttp.bot import Bot @@ -11,7 +12,6 @@ from nonebot.drivers.fastapi import Driver from nonebot.log import logger from nonebot.rule import to_me from nonebot.typing import T_State -import socketio from ..plugin_config import plugin_config from .api import ( @@ -27,21 +27,21 @@ from .api import ( from .jwt import load_jwt from .token_manager import token_manager as tm -URL_BASE = '/bison/' -GLOBAL_CONF_URL = f'{URL_BASE}api/global_conf' -AUTH_URL = f'{URL_BASE}api/auth' -SUBSCRIBE_URL = f'{URL_BASE}api/subs' -GET_TARGET_NAME_URL = f'{URL_BASE}api/target_name' -TEST_URL = f'{URL_BASE}test' +URL_BASE = "/bison/" +GLOBAL_CONF_URL = f"{URL_BASE}api/global_conf" +AUTH_URL = f"{URL_BASE}api/auth" +SUBSCRIBE_URL = f"{URL_BASE}api/subs" +GET_TARGET_NAME_URL = f"{URL_BASE}api/target_name" +TEST_URL = f"{URL_BASE}test" STATIC_PATH = (Path(__file__).parent / "dist").resolve() sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*") socket_app = socketio.ASGIApp(sio, socketio_path="socket") -class SinglePageApplication(StaticFiles): - def __init__(self, directory: os.PathLike, index='index.html'): +class SinglePageApplication(StaticFiles): + def __init__(self, directory: os.PathLike, index="index.html"): self.index = index super().__init__(directory=directory, packages=None, html=True, check_dir=True) @@ -51,12 +51,13 @@ class SinglePageApplication(StaticFiles): return await super().lookup_path(self.index) return (full_path, stat_res) -def register_router_fastapi(driver: Driver, socketio): - from fastapi.security import OAuth2PasswordBearer - from fastapi.param_functions import Depends - from fastapi import HTTPException, status - oath_scheme = OAuth2PasswordBearer(tokenUrl='token') +def register_router_fastapi(driver: Driver, socketio): + from fastapi import HTTPException, status + from fastapi.param_functions import Depends + from fastapi.security import OAuth2PasswordBearer + + oath_scheme = OAuth2PasswordBearer(tokenUrl="token") async def get_jwt_obj(token: str = Depends(oath_scheme)): obj = load_jwt(token) @@ -64,10 +65,12 @@ def register_router_fastapi(driver: Driver, socketio): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED) return obj - async def check_group_permission(groupNumber: str, token_obj: dict = Depends(get_jwt_obj)): - groups = token_obj['groups'] + async def check_group_permission( + groupNumber: str, token_obj: dict = Depends(get_jwt_obj) + ): + groups = token_obj["groups"] for group in groups: - if int(groupNumber) == group['id']: + if int(groupNumber) == group["id"]: return raise HTTPException(status_code=status.HTTP_403_FORBIDDEN) @@ -88,17 +91,35 @@ def register_router_fastapi(driver: Driver, socketio): @app.get(SUBSCRIBE_URL) async def subs(jwt_obj: dict = Depends(get_jwt_obj)): return await get_subs_info(jwt_obj) + @app.get(GET_TARGET_NAME_URL) - async def _get_target_name(platformName: str, target: str, jwt_obj: dict = Depends(get_jwt_obj)): + async def _get_target_name( + platformName: str, target: str, jwt_obj: dict = Depends(get_jwt_obj) + ): return await get_target_name(platformName, target, jwt_obj) + @app.post(SUBSCRIBE_URL, dependencies=[Depends(check_group_permission)]) async def _add_group_subs(groupNumber: str, req: AddSubscribeReq): - return await add_group_sub(group_number=groupNumber, platform_name=req.platformName, - target=req.target, target_name=req.targetName, cats=req.cats, tags=req.tags) + return await add_group_sub( + group_number=groupNumber, + platform_name=req.platformName, + target=req.target, + target_name=req.targetName, + cats=req.cats, + tags=req.tags, + ) + @app.patch(SUBSCRIBE_URL, dependencies=[Depends(check_group_permission)]) async def _update_group_subs(groupNumber: str, req: AddSubscribeReq): - return await update_group_sub(group_number=groupNumber, platform_name=req.platformName, - target=req.target, target_name=req.targetName, cats=req.cats, tags=req.tags) + return await update_group_sub( + group_number=groupNumber, + platform_name=req.platformName, + target=req.target, + target_name=req.targetName, + cats=req.cats, + tags=req.tags, + ) + @app.delete(SUBSCRIBE_URL, dependencies=[Depends(check_group_permission)]) async def _del_group_subs(groupNumber: str, target: str, platformName: str): return await del_group_sub(groupNumber, platformName, target) @@ -108,8 +129,8 @@ def register_router_fastapi(driver: Driver, socketio): def init(): driver = get_driver() - if driver.type == 'fastapi': - assert(isinstance(driver, Driver)) + if driver.type == "fastapi": + assert isinstance(driver, Driver) register_router_fastapi(driver, socket_app) else: logger.warning(f"Driver {driver.type} not supported") @@ -118,19 +139,25 @@ def init(): port = driver.config.port if host in ["0.0.0.0", "127.0.0.1"]: host = "localhost" - logger.opt(colors=True).info(f"Nonebot test frontend will be running at: " - f"http://{host}:{port}{URL_BASE}") + logger.opt(colors=True).info( + f"Nonebot test frontend will be running at: " + f"http://{host}:{port}{URL_BASE}" + ) -if (STATIC_PATH / 'index.html').exists(): + +if (STATIC_PATH / "index.html").exists(): init() - get_token = on_command('后台管理', rule=to_me(), priority=5) + get_token = on_command("后台管理", rule=to_me(), priority=5) + @get_token.handle() async def send_token(bot: "Bot", event: PrivateMessageEvent, state: T_State): token = tm.get_user_token((event.get_user_id(), event.sender.nickname)) - await get_token.finish(f'请访问: {plugin_config.bison_outer_url}auth/{token}') - get_token.__help__name__ = '获取后台管理地址' - get_token.__help__info__ = ('获取管理bot后台的地址,该地址会' - '在一段时间过后过期,请不要泄漏该地址') + await get_token.finish(f"请访问: {plugin_config.bison_outer_url}auth/{token}") + + get_token.__help__name__ = "获取后台管理地址" + get_token.__help__info__ = "获取管理bot后台的地址,该地址会" "在一段时间过后过期,请不要泄漏该地址" else: - logger.warning("Frontend file not found, please compile it or use docker or pypi version") + logger.warning( + "Frontend file not found, please compile it or use docker or pypi version" + ) diff --git a/src/plugins/nonebot_bison/admin_page/api.py b/src/plugins/nonebot_bison/admin_page/api.py index ff17d62..b2f519f 100644 --- a/src/plugins/nonebot_bison/admin_page/api.py +++ b/src/plugins/nonebot_bison/admin_page/api.py @@ -6,110 +6,141 @@ from ..platform import check_sub_target, platform_manager from .jwt import pack_jwt from .token_manager import token_manager + async def test(): return {"status": 200, "text": "test"} + async def get_global_conf(): res = {} for platform_name, platform in platform_manager.items(): res[platform_name] = { - 'platformName': platform_name, - 'categories': platform.categories, - 'enabledTag': platform.enable_tag, - 'name': platform.name, - 'hasTarget': getattr(platform, 'has_target') - } - return { 'platformConf': res } + "platformName": platform_name, + "categories": platform.categories, + "enabledTag": platform.enable_tag, + "name": platform.name, + "hasTarget": getattr(platform, "has_target"), + } + return {"platformConf": res} + async def get_admin_groups(qq: int): bot = nonebot.get_bot() - groups = await bot.call_api('get_group_list') + groups = await bot.call_api("get_group_list") res = [] for group in groups: - group_id = group['group_id'] - users = await bot.call_api('get_group_member_list', group_id=group_id) + group_id = group["group_id"] + users = await bot.call_api("get_group_member_list", group_id=group_id) for user in users: - if user['user_id'] == qq and user['role'] in ('owner', 'admin'): - res.append({'id': group_id, 'name': group['group_name']}) + if user["user_id"] == qq and user["role"] in ("owner", "admin"): + res.append({"id": group_id, "name": group["group_name"]}) return res + async def auth(token: str): if qq_tuple := token_manager.get_user(token): qq, nickname = qq_tuple bot = nonebot.get_bot() - assert(isinstance(bot, Bot)) - groups = await bot.call_api('get_group_list') + assert isinstance(bot, Bot) + groups = await bot.call_api("get_group_list") if str(qq) in nonebot.get_driver().config.superusers: jwt_obj = { - 'id': str(qq), - 'groups': list(map( - lambda info: {'id': info['group_id'], 'name': info['group_name']}, - groups)), - } + "id": str(qq), + "groups": list( + map( + lambda info: { + "id": info["group_id"], + "name": info["group_name"], + }, + groups, + ) + ), + } ret_obj = { - 'type': 'admin', - 'name': nickname, - 'id': str(qq), - 'token': pack_jwt(jwt_obj) - } - return { 'status': 200, **ret_obj } + "type": "admin", + "name": nickname, + "id": str(qq), + "token": pack_jwt(jwt_obj), + } + return {"status": 200, **ret_obj} if admin_groups := await get_admin_groups(int(qq)): - jwt_obj = { - 'id': str(qq), - 'groups': admin_groups - } + jwt_obj = {"id": str(qq), "groups": admin_groups} ret_obj = { - 'type': 'user', - 'name': nickname, - 'id': str(qq), - 'token': pack_jwt(jwt_obj) - } - return { 'status': 200, **ret_obj } + "type": "user", + "name": nickname, + "id": str(qq), + "token": pack_jwt(jwt_obj), + } + return {"status": 200, **ret_obj} else: - return { 'status': 400, 'type': '', 'name': '', 'id': '', 'token': '' } + return {"status": 400, "type": "", "name": "", "id": "", "token": ""} else: - return { 'status': 400, 'type': '', 'name': '', 'id': '', 'token': '' } + return {"status": 400, "type": "", "name": "", "id": "", "token": ""} + async def get_subs_info(jwt_obj: dict): - groups = jwt_obj['groups'] + groups = jwt_obj["groups"] res = {} for group in groups: - group_id = group['id'] + group_id = group["id"] config = Config() - subs = list(map(lambda sub: { - 'platformName': sub['target_type'], 'target': sub['target'], 'targetName': sub['target_name'], 'cats': sub['cats'], 'tags': sub['tags'] - }, config.list_subscribe(group_id, 'group'))) - res[group_id] = { - 'name': group['name'], - 'subscribes': subs - } + subs = list( + map( + lambda sub: { + "platformName": sub["target_type"], + "target": sub["target"], + "targetName": sub["target_name"], + "cats": sub["cats"], + "tags": sub["tags"], + }, + config.list_subscribe(group_id, "group"), + ) + ) + res[group_id] = {"name": group["name"], "subscribes": subs} return res -async def get_target_name(platform_name: str, target: str, jwt_obj: dict): - return {'targetName': await check_sub_target(platform_name, target)} -async def add_group_sub(group_number: str, platform_name: str, target: str, - target_name: str, cats: list[int], tags: list[str]): +async def get_target_name(platform_name: str, target: str, jwt_obj: dict): + return {"targetName": await check_sub_target(platform_name, target)} + + +async def add_group_sub( + group_number: str, + platform_name: str, + target: str, + target_name: str, + cats: list[int], + tags: list[str], +): config = Config() - config.add_subscribe(int(group_number), 'group', target, target_name, platform_name, cats, tags) - return { 'status': 200, 'msg': '' } + config.add_subscribe( + int(group_number), "group", target, target_name, platform_name, cats, tags + ) + return {"status": 200, "msg": ""} + async def del_group_sub(group_number: str, platform_name: str, target: str): config = Config() try: - config.del_subscribe(int(group_number), 'group', target, platform_name) + config.del_subscribe(int(group_number), "group", target, platform_name) except (NoSuchUserException, NoSuchSubscribeException): - return { 'status': 400, 'msg': '删除错误' } - return { 'status': 200, 'msg': '' } + return {"status": 400, "msg": "删除错误"} + return {"status": 200, "msg": ""} -async def update_group_sub(group_number: str, platform_name: str, target: str, - target_name: str, cats: list[int], tags: list[str]): +async def update_group_sub( + group_number: str, + platform_name: str, + target: str, + target_name: str, + cats: list[int], + tags: list[str], +): config = Config() try: - config.update_subscribe(int(group_number), 'group', - target, target_name, platform_name, cats, tags) + config.update_subscribe( + int(group_number), "group", target, target_name, platform_name, cats, tags + ) except (NoSuchUserException, NoSuchSubscribeException): - return { 'status': 400, 'msg': '更新错误' } - return { 'status': 200, 'msg': '' } - + return {"status": 400, "msg": "更新错误"} + return {"status": 200, "msg": ""} diff --git a/src/plugins/nonebot_bison/admin_page/jwt.py b/src/plugins/nonebot_bison/admin_page/jwt.py index c607747..661621a 100644 --- a/src/plugins/nonebot_bison/admin_page/jwt.py +++ b/src/plugins/nonebot_bison/admin_page/jwt.py @@ -1,20 +1,23 @@ +import datetime import random import string from typing import Optional -import jwt -import datetime -_key = ''.join(random.SystemRandom().choice(string.ascii_letters) for _ in range(16)) +import jwt + +_key = "".join(random.SystemRandom().choice(string.ascii_letters) for _ in range(16)) + def pack_jwt(obj: dict) -> str: return jwt.encode( - {'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1), **obj}, - _key, algorithm='HS256' - ) + {"exp": datetime.datetime.utcnow() + datetime.timedelta(hours=1), **obj}, + _key, + algorithm="HS256", + ) + def load_jwt(token: str) -> Optional[dict]: try: - return jwt.decode(token, _key, algorithms=['HS256']) + return jwt.decode(token, _key, algorithms=["HS256"]) except: return None - diff --git a/src/plugins/nonebot_bison/admin_page/token_manager.py b/src/plugins/nonebot_bison/admin_page/token_manager.py index 47e3927..e540656 100644 --- a/src/plugins/nonebot_bison/admin_page/token_manager.py +++ b/src/plugins/nonebot_bison/admin_page/token_manager.py @@ -1,24 +1,26 @@ -from typing import Optional -from expiringdict import ExpiringDict import random -import string +import string +from typing import Optional + +from expiringdict import ExpiringDict + class TokenManager: - def __init__(self): - self.token_manager = ExpiringDict(max_len=100, max_age_seconds=60*10) + self.token_manager = ExpiringDict(max_len=100, max_age_seconds=60 * 10) def get_user(self, token: str) -> Optional[tuple]: res = self.token_manager.get(token) - assert(res is None or isinstance(res, tuple)) + assert res is None or isinstance(res, tuple) return res def save_user(self, token: str, qq: tuple) -> None: self.token_manager[token] = qq def get_user_token(self, qq: tuple) -> str: - token = ''.join(random.choices(string.ascii_letters + string.digits, k=16)) + token = "".join(random.choices(string.ascii_letters + string.digits, k=16)) self.save_user(token, qq) return token + token_manager = TokenManager() diff --git a/src/plugins/nonebot_bison/config.py b/src/plugins/nonebot_bison/config.py index 0712fcb..a021aeb 100644 --- a/src/plugins/nonebot_bison/config.py +++ b/src/plugins/nonebot_bison/config.py @@ -1,6 +1,6 @@ +import os from collections import defaultdict from os import path -import os from typing import DefaultDict, Literal, Mapping, TypedDict import nonebot @@ -14,26 +14,30 @@ from .utils import Singleton supported_target_type = platform_manager.keys() + def get_config_path() -> str: if plugin_config.bison_config_path: data_dir = plugin_config.bison_config_path else: working_dir = os.getcwd() - data_dir = path.join(working_dir, 'data') + data_dir = path.join(working_dir, "data") if not path.isdir(data_dir): os.makedirs(data_dir) - old_path = path.join(data_dir, 'hk_reporter.json') - new_path = path.join(data_dir, 'bison.json') + old_path = path.join(data_dir, "hk_reporter.json") + new_path = path.join(data_dir, "bison.json") if os.path.exists(old_path) and not os.path.exists(new_path): os.rename(old_path, new_path) return new_path + class NoSuchUserException(Exception): pass + class NoSuchSubscribeException(Exception): pass + class SubscribeContent(TypedDict): target: str target_type: str @@ -41,75 +45,105 @@ class SubscribeContent(TypedDict): cats: list[int] tags: list[str] + class ConfigContent(TypedDict): user: str user_type: Literal["group", "private"] subs: list[SubscribeContent] + class Config(metaclass=Singleton): migrate_version = 2 - + def __init__(self): - self.db = TinyDB(get_config_path(), encoding='utf-8') - self.kv_config = self.db.table('kv') - self.user_target = self.db.table('user_target') + self.db = TinyDB(get_config_path(), encoding="utf-8") + self.kv_config = self.db.table("kv") + self.user_target = self.db.table("user_target") self.target_user_cache: dict[str, defaultdict[Target, list[User]]] = {} self.target_user_cat_cache = {} self.target_user_tag_cache = {} self.target_list = {} self.next_index: DefaultDict[str, int] = defaultdict(lambda: 0) - - def add_subscribe(self, user, user_type, target, target_name, target_type, cats, tags): + + def add_subscribe( + self, user, user_type, target, target_name, target_type, cats, tags + ): user_query = Query() query = (user_query.user == user) & (user_query.user_type == user_type) - if (user_data := self.user_target.get(query)): + if user_data := self.user_target.get(query): # update - subs: list = user_data.get('subs', []) - subs.append({"target": target, "target_type": target_type, 'target_name': target_name, 'cats': cats, 'tags': tags}) + subs: list = user_data.get("subs", []) + subs.append( + { + "target": target, + "target_type": target_type, + "target_name": target_name, + "cats": cats, + "tags": tags, + } + ) self.user_target.update({"subs": subs}, query) else: # insert - self.user_target.insert({ - 'user': user, 'user_type': user_type, - 'subs': [{'target': target, 'target_type': target_type, 'target_name': target_name, 'cats': cats, 'tags': tags }] - }) + self.user_target.insert( + { + "user": user, + "user_type": user_type, + "subs": [ + { + "target": target, + "target_type": target_type, + "target_name": target_name, + "cats": cats, + "tags": tags, + } + ], + } + ) self.update_send_cache() def list_subscribe(self, user, user_type) -> list[SubscribeContent]: query = Query() - if user_sub := self.user_target.get((query.user == user) & (query.user_type ==user_type)): - return user_sub['subs'] + if user_sub := self.user_target.get( + (query.user == user) & (query.user_type == user_type) + ): + return user_sub["subs"] return [] def get_all_subscribe(self): return self.user_target - + def del_subscribe(self, user, user_type, target, target_type): user_query = Query() query = (user_query.user == user) & (user_query.user_type == user_type) if not (query_res := self.user_target.get(query)): raise NoSuchUserException() - subs = query_res.get('subs', []) + subs = query_res.get("subs", []) for idx, sub in enumerate(subs): - if sub.get('target') == target and sub.get('target_type') == target_type: + if sub.get("target") == target and sub.get("target_type") == target_type: subs.pop(idx) - self.user_target.update({'subs': subs}, query) + self.user_target.update({"subs": subs}, query) self.update_send_cache() return raise NoSuchSubscribeException() - def update_subscribe(self, user, user_type, target, target_name, target_type, cats, tags): + def update_subscribe( + self, user, user_type, target, target_name, target_type, cats, tags + ): user_query = Query() query = (user_query.user == user) & (user_query.user_type == user_type) - if (user_data := self.user_target.get(query)): + if user_data := self.user_target.get(query): # update - subs: list = user_data.get('subs', []) + subs: list = user_data.get("subs", []) find_flag = False for item in subs: - if item['target'] == target and item['target_type'] == target_type: - item['target_name'], item['cats'], item['tags'] = \ - target_name, cats, tags + if item["target"] == target and item["target_type"] == target_type: + item["target_name"], item["cats"], item["tags"] = ( + target_name, + cats, + tags, + ) find_flag = True break if not find_flag: @@ -121,33 +155,58 @@ class Config(metaclass=Singleton): def update_send_cache(self): res = {target_type: defaultdict(list) for target_type in supported_target_type} - cat_res = {target_type: defaultdict(lambda: defaultdict(list)) for target_type in supported_target_type} - tag_res = {target_type: defaultdict(lambda: defaultdict(list)) for target_type in supported_target_type} + cat_res = { + target_type: defaultdict(lambda: defaultdict(list)) + for target_type in supported_target_type + } + tag_res = { + target_type: defaultdict(lambda: defaultdict(list)) + for target_type in supported_target_type + } # res = {target_type: defaultdict(lambda: defaultdict(list)) for target_type in supported_target_type} to_del = [] for user in self.user_target.all(): - for sub in user.get('subs', []): - if not sub.get('target_type') in supported_target_type: - to_del.append({'user': user['user'], 'user_type': user['user_type'], 'target': sub['target'], 'target_type': sub['target_type']}) + for sub in user.get("subs", []): + if not sub.get("target_type") in supported_target_type: + to_del.append( + { + "user": user["user"], + "user_type": user["user_type"], + "target": sub["target"], + "target_type": sub["target_type"], + } + ) continue - res[sub['target_type']][sub['target']].append(User(user['user'], user['user_type'])) - cat_res[sub['target_type']][sub['target']]['{}-{}'.format(user['user_type'], user['user'])] = sub['cats'] - tag_res[sub['target_type']][sub['target']]['{}-{}'.format(user['user_type'], user['user'])] = sub['tags'] + res[sub["target_type"]][sub["target"]].append( + User(user["user"], user["user_type"]) + ) + cat_res[sub["target_type"]][sub["target"]][ + "{}-{}".format(user["user_type"], user["user"]) + ] = sub["cats"] + tag_res[sub["target_type"]][sub["target"]][ + "{}-{}".format(user["user_type"], user["user"]) + ] = sub["tags"] self.target_user_cache = res self.target_user_cat_cache = cat_res self.target_user_tag_cache = tag_res for target_type in self.target_user_cache: - self.target_list[target_type] = list(self.target_user_cache[target_type].keys()) - - logger.info(f'Deleting {to_del}') + self.target_list[target_type] = list( + self.target_user_cache[target_type].keys() + ) + + logger.info(f"Deleting {to_del}") for d in to_del: self.del_subscribe(**d) def get_sub_category(self, target_type, target, user_type, user): - return self.target_user_cat_cache[target_type][target]['{}-{}'.format(user_type, user)] + return self.target_user_cat_cache[target_type][target][ + "{}-{}".format(user_type, user) + ] def get_sub_tags(self, target_type, target, user_type, user): - return self.target_user_tag_cache[target_type][target]['{}-{}'.format(user_type, user)] + return self.target_user_tag_cache[target_type][target][ + "{}-{}".format(user_type, user) + ] def get_next_target(self, target_type): # FIXME 插入或删除target后对队列的影响(但是并不是大问题 @@ -158,25 +217,27 @@ class Config(metaclass=Singleton): self.next_index[target_type] += 1 return res + def start_up(): config = Config() - if not (search_res := config.kv_config.search(Query().name=="version")): + if not (search_res := config.kv_config.search(Query().name == "version")): config.kv_config.insert({"name": "version", "value": config.migrate_version}) elif search_res[0].get("value") < config.migrate_version: query = Query() - version_query = (query.name == 'version') + version_query = query.name == "version" cur_version = search_res[0].get("value") if cur_version == 1: cur_version = 2 for user_conf in config.user_target.all(): conf_id = user_conf.doc_id - subs = user_conf['subs'] + subs = user_conf["subs"] for sub in subs: - sub['cats'] = [] - sub['tags'] = [] - config.user_target.update({'subs': subs}, doc_ids=[conf_id]) + sub["cats"] = [] + sub["tags"] = [] + config.user_target.update({"subs": subs}, doc_ids=[conf_id]) config.kv_config.update({"value": config.migrate_version}, version_query) # do migration config.update_send_cache() + nonebot.get_driver().on_startup(start_up) diff --git a/src/plugins/nonebot_bison/config_manager.py b/src/plugins/nonebot_bison/config_manager.py index 436c0c6..8e96ea6 100644 --- a/src/plugins/nonebot_bison/config_manager.py +++ b/src/plugins/nonebot_bison/config_manager.py @@ -7,7 +7,7 @@ from nonebot.adapters.cqhttp import Bot, Event, GroupMessageEvent from nonebot.adapters.cqhttp.message import Message from nonebot.adapters.cqhttp.permission import GROUP_ADMIN, GROUP_MEMBER, GROUP_OWNER from nonebot.matcher import Matcher -from nonebot.permission import Permission, SUPERUSER +from nonebot.permission import SUPERUSER, Permission from nonebot.rule import to_me from nonebot.typing import T_State @@ -16,137 +16,184 @@ from .platform import check_sub_target, platform_manager from .types import Target from .utils import parse_text + def _gen_prompt_template(prompt: str): - if hasattr(Message, 'template'): + if hasattr(Message, "template"): return Message.template(prompt) return prompt -common_platform = [p.platform_name for p in \ - filter(lambda platform: platform.enabled and platform.is_common, - platform_manager.values()) - ] -help_match = on_command('help', rule=to_me(), priority=5) +common_platform = [ + p.platform_name + for p in filter( + lambda platform: platform.enabled and platform.is_common, + platform_manager.values(), + ) +] + +help_match = on_command("help", rule=to_me(), priority=5) + + @help_match.handle() async def send_help(bot: Bot, event: Event, state: T_State): - message = '使用方法:\n@bot 添加订阅(仅管理员)\n@bot 查询订阅\n@bot 删除订阅(仅管理员)' + message = "使用方法:\n@bot 添加订阅(仅管理员)\n@bot 查询订阅\n@bot 删除订阅(仅管理员)" await help_match.finish(Message(await parse_text(message))) def do_add_sub(add_sub: Type[Matcher]): @add_sub.handle() async def init_promote(bot: Bot, event: Event, state: T_State): - state['_prompt'] = '请输入想要订阅的平台,目前支持,请输入冒号左边的名称:\n' + \ - ''.join(['{}:{}\n'.format(platform_name, platform_manager[platform_name].name) \ - for platform_name in common_platform]) + \ - '要查看全部平台请输入:“全部”' + state["_prompt"] = ( + "请输入想要订阅的平台,目前支持,请输入冒号左边的名称:\n" + + "".join( + [ + "{}:{}\n".format( + platform_name, platform_manager[platform_name].name + ) + for platform_name in common_platform + ] + ) + + "要查看全部平台请输入:“全部”" + ) - async def parse_platform(bot: AbstractBot, event: AbstractEvent, state: T_State) -> None: + async def parse_platform( + bot: AbstractBot, event: AbstractEvent, state: T_State + ) -> None: platform = str(event.get_message()).strip() - if platform == '全部': - message = '全部平台\n' + \ - '\n'.join(['{}:{}'.format(platform_name, platform.name) \ - for platform_name, platform in platform_manager.items()]) + if platform == "全部": + message = "全部平台\n" + "\n".join( + [ + "{}:{}".format(platform_name, platform.name) + for platform_name, platform in platform_manager.items() + ] + ) await add_sub.reject(message) elif platform in platform_manager: - state['platform'] = platform + state["platform"] = platform else: - await add_sub.reject('平台输入错误') + await add_sub.reject("平台输入错误") - @add_sub.got('platform', _gen_prompt_template('{_prompt}'), parse_platform) + @add_sub.got("platform", _gen_prompt_template("{_prompt}"), parse_platform) @add_sub.handle() async def init_id(bot: Bot, event: Event, state: T_State): - if platform_manager[state['platform']].has_target: - state['_prompt'] = '请输入订阅用户的id,详情查阅https://nonebot-bison.vercel.app/usage/#%E6%89%80%E6%94%AF%E6%8C%81%E5%B9%B3%E5%8F%B0%E7%9A%84uid' + if platform_manager[state["platform"]].has_target: + state[ + "_prompt" + ] = "请输入订阅用户的id,详情查阅https://nonebot-bison.vercel.app/usage/#%E6%89%80%E6%94%AF%E6%8C%81%E5%B9%B3%E5%8F%B0%E7%9A%84uid" else: - state['id'] = 'default' - state['name'] = await platform_manager[state['platform']].get_target_name(Target('')) + state["id"] = "default" + state["name"] = await platform_manager[state["platform"]].get_target_name( + Target("") + ) async def parse_id(bot: AbstractBot, event: AbstractEvent, state: T_State): target = str(event.get_message()).strip() try: - name = await check_sub_target(state['platform'], target) + name = await check_sub_target(state["platform"], target) if not name: - await add_sub.reject('id输入错误') - state['id'] = target - state['name'] = name + await add_sub.reject("id输入错误") + state["id"] = target + state["name"] = name except: - await add_sub.reject('id输入错误') + await add_sub.reject("id输入错误") - @add_sub.got('id', _gen_prompt_template('{_prompt}'), parse_id) + @add_sub.got("id", _gen_prompt_template("{_prompt}"), parse_id) @add_sub.handle() async def init_cat(bot: Bot, event: Event, state: T_State): - if not platform_manager[state['platform']].categories: - state['cats'] = [] + if not platform_manager[state["platform"]].categories: + state["cats"] = [] return - state['_prompt'] = '请输入要订阅的类别,以空格分隔,支持的类别有:{}'.format( - ' '.join(list(platform_manager[state['platform']].categories.values()))) + state["_prompt"] = "请输入要订阅的类别,以空格分隔,支持的类别有:{}".format( + " ".join(list(platform_manager[state["platform"]].categories.values())) + ) async def parser_cats(bot: AbstractBot, event: AbstractEvent, state: T_State): res = [] for cat in str(event.get_message()).strip().split(): - if cat not in platform_manager[state['platform']].reverse_category: - await add_sub.reject('不支持 {}'.format(cat)) - res.append(platform_manager[state['platform']].reverse_category[cat]) - state['cats'] = res + if cat not in platform_manager[state["platform"]].reverse_category: + await add_sub.reject("不支持 {}".format(cat)) + res.append(platform_manager[state["platform"]].reverse_category[cat]) + state["cats"] = res - @add_sub.got('cats', _gen_prompt_template('{_prompt}'), parser_cats) + @add_sub.got("cats", _gen_prompt_template("{_prompt}"), parser_cats) @add_sub.handle() async def init_tag(bot: Bot, event: Event, state: T_State): - if not platform_manager[state['platform']].enable_tag: - state['tags'] = [] + if not platform_manager[state["platform"]].enable_tag: + state["tags"] = [] return - state['_prompt'] = '请输入要订阅的tag,订阅所有tag输入"全部标签"' + state["_prompt"] = '请输入要订阅的tag,订阅所有tag输入"全部标签"' async def parser_tags(bot: AbstractBot, event: AbstractEvent, state: T_State): - if str(event.get_message()).strip() == '全部标签': - state['tags'] = [] + if str(event.get_message()).strip() == "全部标签": + state["tags"] = [] else: - state['tags'] = str(event.get_message()).strip().split() + state["tags"] = str(event.get_message()).strip().split() - @add_sub.got('tags', _gen_prompt_template('{_prompt}'), parser_tags) + @add_sub.got("tags", _gen_prompt_template("{_prompt}"), parser_tags) @add_sub.handle() async def add_sub_process(bot: Bot, event: Event, state: T_State): config = Config() - config.add_subscribe(state.get('_user_id') or event.group_id, user_type='group', - target=state['id'], - target_name=state['name'], target_type=state['platform'], - cats=state.get('cats', []), tags=state.get('tags', [])) - await add_sub.finish('添加 {} 成功'.format(state['name'])) + config.add_subscribe( + state.get("_user_id") or event.group_id, + user_type="group", + target=state["id"], + target_name=state["name"], + target_type=state["platform"], + cats=state.get("cats", []), + tags=state.get("tags", []), + ) + await add_sub.finish("添加 {} 成功".format(state["name"])) + def do_query_sub(query_sub: Type[Matcher]): @query_sub.handle() async def _(bot: Bot, event: Event, state: T_State): config: Config = Config() - sub_list = config.list_subscribe(state.get('_user_id') or event.group_id, "group") - res = '订阅的帐号为:\n' + sub_list = config.list_subscribe( + state.get("_user_id") or event.group_id, "group" + ) + res = "订阅的帐号为:\n" for sub in sub_list: - res += '{} {} {}'.format(sub['target_type'], sub['target_name'], sub['target']) - platform = platform_manager[sub['target_type']] + res += "{} {} {}".format( + sub["target_type"], sub["target_name"], sub["target"] + ) + platform = platform_manager[sub["target_type"]] if platform.categories: - res += ' [{}]'.format(', '.join(map(lambda x: platform.categories[x], sub['cats']))) + res += " [{}]".format( + ", ".join(map(lambda x: platform.categories[x], sub["cats"])) + ) if platform.enable_tag: - res += ' {}'.format(', '.join(sub['tags'])) - res += '\n' + res += " {}".format(", ".join(sub["tags"])) + res += "\n" await query_sub.finish(Message(await parse_text(res))) + def do_del_sub(del_sub: Type[Matcher]): @del_sub.handle() async def send_list(bot: Bot, event: Event, state: T_State): config: Config = Config() - sub_list = config.list_subscribe(state.get('_user_id') or event.group_id, "group") - res = '订阅的帐号为:\n' - state['sub_table'] = {} + sub_list = config.list_subscribe( + state.get("_user_id") or event.group_id, "group" + ) + res = "订阅的帐号为:\n" + state["sub_table"] = {} for index, sub in enumerate(sub_list, 1): - state['sub_table'][index] = {'target_type': sub['target_type'], 'target': sub['target']} - res += '{} {} {} {}\n'.format(index, sub['target_type'], sub['target_name'], sub['target']) - platform = platform_manager[sub['target_type']] + state["sub_table"][index] = { + "target_type": sub["target_type"], + "target": sub["target"], + } + res += "{} {} {} {}\n".format( + index, sub["target_type"], sub["target_name"], sub["target"] + ) + platform = platform_manager[sub["target_type"]] if platform.categories: - res += ' [{}]'.format(', '.join(map(lambda x: platform.categories[x], sub['cats']))) + res += " [{}]".format( + ", ".join(map(lambda x: platform.categories[x], sub["cats"])) + ) if platform.enable_tag: - res += ' {}'.format(', '.join(sub['tags'])) - res += '\n' - res += '请输入要删除的订阅的序号' + res += " {}".format(", ".join(sub["tags"])) + res += "\n" + res += "请输入要删除的订阅的序号" await bot.send(event=event, message=Message(await parse_text(res))) @del_sub.receive() @@ -154,39 +201,60 @@ def do_del_sub(del_sub: Type[Matcher]): try: index = int(str(event.get_message()).strip()) config = Config() - config.del_subscribe(state.get('_user_id') or event.group_id, 'group', **state['sub_table'][index]) + config.del_subscribe( + state.get("_user_id") or event.group_id, + "group", + **state["sub_table"][index] + ) except Exception as e: - await del_sub.reject('删除错误') + await del_sub.reject("删除错误") logger.warning(e) else: - await del_sub.finish('删除成功') + await del_sub.finish("删除成功") + async def parse_group_number(bot: AbstractBot, event: AbstractEvent, state: T_State): state[state["_current_key"]] = int(str(event.get_message())) -add_sub_matcher = on_command("添加订阅", rule=to_me(), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER, priority=5) +add_sub_matcher = on_command( + "添加订阅", rule=to_me(), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER, priority=5 +) do_add_sub(add_sub_matcher) -manage_add_sub_mather = on_command('管理-添加订阅', permission=SUPERUSER, priority=5) -@manage_add_sub_mather.got('_user_id', "群号", parse_group_number) +manage_add_sub_mather = on_command("管理-添加订阅", permission=SUPERUSER, priority=5) + + +@manage_add_sub_mather.got("_user_id", "群号", parse_group_number) async def handle(bot: Bot, event: Event, state: T_State): pass + + do_add_sub(manage_add_sub_mather) query_sub_macher = on_command("查询订阅", rule=to_me(), priority=5) do_query_sub(query_sub_macher) -manage_query_sub_mather = on_command('管理-查询订阅', permission=SUPERUSER, priority=5) -@manage_query_sub_mather.got('_user_id', "群号", parse_group_number) +manage_query_sub_mather = on_command("管理-查询订阅", permission=SUPERUSER, priority=5) + + +@manage_query_sub_mather.got("_user_id", "群号", parse_group_number) async def handle(bot: Bot, event: Event, state: T_State): pass + + do_query_sub(manage_query_sub_mather) -del_sub_macher = on_command("删除订阅", rule=to_me(), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER, priority=5) +del_sub_macher = on_command( + "删除订阅", rule=to_me(), permission=GROUP_ADMIN | GROUP_OWNER | SUPERUSER, priority=5 +) do_del_sub(del_sub_macher) -manage_del_sub_mather = on_command('管理-删除订阅', permission=SUPERUSER, priority=5) -@manage_del_sub_mather.got('_user_id', "群号", parse_group_number) +manage_del_sub_mather = on_command("管理-删除订阅", permission=SUPERUSER, priority=5) + + +@manage_del_sub_mather.got("_user_id", "群号", parse_group_number) async def handle(bot: Bot, event: Event, state: T_State): pass + + do_del_sub(manage_del_sub_mather) diff --git a/src/plugins/nonebot_bison/platform/__init__.py b/src/plugins/nonebot_bison/platform/__init__.py index 028f002..60b5e32 100644 --- a/src/plugins/nonebot_bison/platform/__init__.py +++ b/src/plugins/nonebot_bison/platform/__init__.py @@ -1,18 +1,19 @@ from collections import defaultdict - -from .platform import Platform, NoTargetGroup -from pkgutil import iter_modules -from pathlib import Path from importlib import import_module +from pathlib import Path +from pkgutil import iter_modules + +from .platform import NoTargetGroup, Platform _package_dir = str(Path(__file__).resolve().parent) for (_, module_name, _) in iter_modules([_package_dir]): - import_module(f'{__name__}.{module_name}') + import_module(f"{__name__}.{module_name}") async def check_sub_target(target_type, target): return await platform_manager[target_type].get_target_name(target) + _platform_list = defaultdict(list) for _platform in Platform.registry: if not _platform.enabled: @@ -24,5 +25,6 @@ for name, platform_list in _platform_list.items(): if len(platform_list) == 1: platform_manager[name] = platform_list[0]() else: - platform_manager[name] = NoTargetGroup([_platform() for _platform in platform_list]) - + platform_manager[name] = NoTargetGroup( + [_platform() for _platform in platform_list] + ) diff --git a/src/plugins/nonebot_bison/platform/arknights.py b/src/plugins/nonebot_bison/platform/arknights.py index fb25729..f7b0002 100644 --- a/src/plugins/nonebot_bison/platform/arknights.py +++ b/src/plugins/nonebot_bison/platform/arknights.py @@ -1,8 +1,8 @@ import json from typing import Any -from bs4 import BeautifulSoup as bs import httpx +from bs4 import BeautifulSoup as bs from ..post import Post from ..types import Category, RawPost, Target @@ -12,26 +12,28 @@ from .platform import CategoryNotSupport, NewMessage, StatusChange class Arknights(NewMessage): - categories = {1: '游戏公告'} - platform_name = 'arknights' - name = '明日方舟游戏信息' + categories = {1: "游戏公告"} + platform_name = "arknights" + name = "明日方舟游戏信息" enable_tag = False enabled = True is_common = False - schedule_type = 'interval' - schedule_kw = {'seconds': 30} + schedule_type = "interval" + schedule_kw = {"seconds": 30} has_target = False async def get_target_name(self, _: Target) -> str: - return '明日方舟游戏信息' + return "明日方舟游戏信息" async def get_sub_list(self, _) -> list[RawPost]: async with httpx.AsyncClient() as client: - raw_data = await client.get('https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/announcement.meta.json') - return json.loads(raw_data.text)['announceList'] + raw_data = await client.get( + "https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/announcement.meta.json" + ) + return json.loads(raw_data.text)["announceList"] def get_id(self, post: RawPost) -> Any: - return post['announceId'] + return post["announceId"] def get_date(self, _: RawPost) -> None: return None @@ -40,64 +42,85 @@ class Arknights(NewMessage): return Category(1) async def parse(self, raw_post: RawPost) -> Post: - announce_url = raw_post['webUrl'] - text = '' + announce_url = raw_post["webUrl"] + text = "" async with httpx.AsyncClient() as client: raw_html = await client.get(announce_url) - soup = bs(raw_html, 'html.parser') + soup = bs(raw_html, "html.parser") pics = [] if soup.find("div", class_="standerd-container"): # 图文 render = Render() - viewport = {'width': 320, 'height': 6400, 'deviceScaleFactor': 3} - pic_data = await render.render(announce_url, viewport=viewport, target='div.main') + viewport = {"width": 320, "height": 6400, "deviceScaleFactor": 3} + pic_data = await render.render( + announce_url, viewport=viewport, target="div.main" + ) if pic_data: pics.append(pic_data) else: - text = '图片渲染失败' - elif (pic := soup.find('img', class_='banner-image')): - pics.append(pic['src']) + text = "图片渲染失败" + elif pic := soup.find("img", class_="banner-image"): + pics.append(pic["src"]) else: raise CategoryNotSupport() - return Post('arknights', text=text, url='', target_name="明日方舟游戏内公告", pics=pics, compress=True, override_use_pic=False) + return Post( + "arknights", + text=text, + url="", + target_name="明日方舟游戏内公告", + pics=pics, + compress=True, + override_use_pic=False, + ) + class AkVersion(StatusChange): - categories = {2: '更新信息'} - platform_name = 'arknights' - name = '明日方舟游戏信息' + categories = {2: "更新信息"} + platform_name = "arknights" + name = "明日方舟游戏信息" enable_tag = False enabled = True is_common = False - schedule_type = 'interval' - schedule_kw = {'seconds': 30} + schedule_type = "interval" + schedule_kw = {"seconds": 30} has_target = False async def get_target_name(self, _: Target) -> str: - return '明日方舟游戏信息' + return "明日方舟游戏信息" async def get_status(self, _): async with httpx.AsyncClient() as client: - res_ver = await client.get('https://ak-conf.hypergryph.com/config/prod/official/IOS/version') - res_preanounce = await client.get('https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/preannouncement.meta.json') + res_ver = await client.get( + "https://ak-conf.hypergryph.com/config/prod/official/IOS/version" + ) + res_preanounce = await client.get( + "https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/preannouncement.meta.json" + ) res = res_ver.json() res.update(res_preanounce.json()) return res def compare_status(self, _, old_status, new_status): res = [] - if old_status.get('preAnnounceType') == 2 and new_status.get('preAnnounceType') == 0: - res.append(Post('arknights', - text='登录界面维护公告上线(大概是开始维护了)', - target_name='明日方舟更新信息')) - elif old_status.get('preAnnounceType') == 0 and new_status.get('preAnnounceType') == 2: - res.append(Post('arknights', - text='登录界面维护公告下线(大概是开服了,冲!)', - target_name='明日方舟更新信息')) - if old_status.get('clientVersion') != new_status.get('clientVersion'): - res.append(Post('arknights', text='游戏本体更新(大更新)', target_name='明日方舟更新信息')) - if old_status.get('resVersion') != new_status.get('resVersion'): - res.append(Post('arknights', text='游戏资源更新(小更新)', target_name='明日方舟更新信息')) + if ( + old_status.get("preAnnounceType") == 2 + and new_status.get("preAnnounceType") == 0 + ): + res.append( + Post("arknights", text="登录界面维护公告上线(大概是开始维护了)", target_name="明日方舟更新信息") + ) + elif ( + old_status.get("preAnnounceType") == 0 + and new_status.get("preAnnounceType") == 2 + ): + res.append( + Post("arknights", text="登录界面维护公告下线(大概是开服了,冲!)", target_name="明日方舟更新信息") + ) + if old_status.get("clientVersion") != new_status.get("clientVersion"): + res.append(Post("arknights", text="游戏本体更新(大更新)", target_name="明日方舟更新信息")) + if old_status.get("resVersion") != new_status.get("resVersion"): + res.append(Post("arknights", text="游戏资源更新(小更新)", target_name="明日方舟更新信息")) return res def get_category(self, _): @@ -106,28 +129,29 @@ class AkVersion(StatusChange): async def parse(self, raw_post): return raw_post + class MonsterSiren(NewMessage): - categories = {3: '塞壬唱片新闻'} - platform_name = 'arknights' - name = '明日方舟游戏信息' + categories = {3: "塞壬唱片新闻"} + platform_name = "arknights" + name = "明日方舟游戏信息" enable_tag = False enabled = True is_common = False - schedule_type = 'interval' - schedule_kw = {'seconds': 30} + schedule_type = "interval" + schedule_kw = {"seconds": 30} has_target = False async def get_target_name(self, _: Target) -> str: - return '明日方舟游戏信息' + return "明日方舟游戏信息" 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'] + raw_data = await client.get("https://monster-siren.hypergryph.com/api/news") + return raw_data.json()["data"]["list"] def get_id(self, post: RawPost) -> Any: - return post['cid'] + return post["cid"] def get_date(self, _) -> None: return None @@ -138,13 +162,21 @@ class MonsterSiren(NewMessage): async def parse(self, raw_post: RawPost) -> Post: url = f'https://monster-siren.hypergryph.com/info/{raw_post["cid"]}' async with httpx.AsyncClient() as client: - res = await client.get(f'https://monster-siren.hypergryph.com/api/news/{raw_post["cid"]}') + res = await client.get( + f'https://monster-siren.hypergryph.com/api/news/{raw_post["cid"]}' + ) raw_data = res.json() - content = raw_data['data']['content'] - content = content.replace('

', '

\n') - soup = bs(content, 'html.parser') - imgs = list(map(lambda x: x['src'], soup('img'))) + content = raw_data["data"]["content"] + content = content.replace("

", "

\n") + soup = bs(content, "html.parser") + imgs = list(map(lambda x: x["src"], soup("img"))) text = f'{raw_post["title"]}\n{soup.text.strip()}' - return Post('monster-siren', text=text, pics=imgs, - url=url, target_name="塞壬唱片新闻", compress=True, - override_use_pic=False) + return Post( + "monster-siren", + text=text, + pics=imgs, + url=url, + target_name="塞壬唱片新闻", + compress=True, + override_use_pic=False, + ) diff --git a/src/plugins/nonebot_bison/platform/bilibili.py b/src/plugins/nonebot_bison/platform/bilibili.py index 8980eb1..5ff3cc1 100644 --- a/src/plugins/nonebot_bison/platform/bilibili.py +++ b/src/plugins/nonebot_bison/platform/bilibili.py @@ -5,50 +5,57 @@ import httpx from ..post import Post from ..types import Category, RawPost, Tag, Target -from .platform import NewMessage, CategoryNotSupport +from .platform import CategoryNotSupport, NewMessage + class Bilibili(NewMessage): categories = { - 1: "一般动态", - 2: "专栏文章", - 3: "视频", - 4: "纯文字", - 5: "转发" - # 5: "短视频" - } - platform_name = 'bilibili' + 1: "一般动态", + 2: "专栏文章", + 3: "视频", + 4: "纯文字", + 5: "转发" + # 5: "短视频" + } + platform_name = "bilibili" enable_tag = True enabled = True is_common = True - schedule_type = 'interval' - schedule_kw = {'seconds': 10} - name = 'B站' + schedule_type = "interval" + schedule_kw = {"seconds": 10} + name = "B站" has_target = True async def get_target_name(self, target: Target) -> Optional[str]: async with httpx.AsyncClient() as client: - res = await client.get('https://api.bilibili.com/x/space/acc/info', params={'mid': target}) + res = await client.get( + "https://api.bilibili.com/x/space/acc/info", params={"mid": target} + ) res_data = json.loads(res.text) - if res_data['code']: + if res_data["code"]: return None - return res_data['data']['name'] + return res_data["data"]["name"] async def get_sub_list(self, target: Target) -> list[RawPost]: async with httpx.AsyncClient() as client: - params = {'host_uid': target, 'offset': 0, 'need_top': 0} - res = await client.get('https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history', params=params, timeout=4.0) + params = {"host_uid": target, "offset": 0, "need_top": 0} + res = await client.get( + "https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/space_history", + params=params, + timeout=4.0, + ) res_dict = json.loads(res.text) - if res_dict['code'] == 0: - return res_dict['data']['cards'] + if res_dict["code"] == 0: + return res_dict["data"]["cards"] else: return [] def get_id(self, post: RawPost) -> Any: - return post['desc']['dynamic_id'] - + return post["desc"]["dynamic_id"] + def get_date(self, post: RawPost) -> int: - return post['desc']['timestamp'] + return post["desc"]["timestamp"] def _do_get_category(self, post_type: int) -> Category: if post_type == 2: @@ -65,63 +72,75 @@ class Bilibili(NewMessage): raise CategoryNotSupport() def get_category(self, post: RawPost) -> Category: - post_type = post['desc']['type'] + post_type = post["desc"]["type"] return self._do_get_category(post_type) def get_tags(self, raw_post: RawPost) -> list[Tag]: - return [*map(lambda tp: tp['topic_name'], raw_post['display']['topic_info']['topic_details'])] + return [ + *map( + lambda tp: tp["topic_name"], + raw_post["display"]["topic_info"]["topic_details"], + ) + ] def _get_info(self, post_type: Category, card) -> tuple[str, list]: if post_type == 1: # 一般动态 - text = card['item']['description'] - pic = [img['img_src'] for img in card['item']['pictures']] + text = card["item"]["description"] + pic = [img["img_src"] for img in card["item"]["pictures"]] elif post_type == 2: # 专栏文章 - text = '{} {}'.format(card['title'], card['summary']) - pic = card['image_urls'] + text = "{} {}".format(card["title"], card["summary"]) + pic = card["image_urls"] elif post_type == 3: # 视频 - text = card['dynamic'] - pic = [card['pic']] + text = card["dynamic"] + pic = [card["pic"]] elif post_type == 4: # 纯文字 - text = card['item']['content'] + text = card["item"]["content"] pic = [] else: raise CategoryNotSupport() return text, pic async def parse(self, raw_post: RawPost) -> Post: - card_content = json.loads(raw_post['card']) + card_content = json.loads(raw_post["card"]) post_type = self.get_category(raw_post) - target_name = raw_post['desc']['user_profile']['info']['uname'] + target_name = raw_post["desc"]["user_profile"]["info"]["uname"] if post_type >= 1 and post_type < 5: - url = '' + url = "" if post_type == 1: # 一般动态 - url = 'https://t.bilibili.com/{}'.format(raw_post['desc']['dynamic_id_str']) + url = "https://t.bilibili.com/{}".format( + raw_post["desc"]["dynamic_id_str"] + ) elif post_type == 2: # 专栏文章 - url = 'https://www.bilibili.com/read/cv{}'.format(raw_post['desc']['rid']) + url = "https://www.bilibili.com/read/cv{}".format( + raw_post["desc"]["rid"] + ) elif post_type == 3: # 视频 - url = 'https://www.bilibili.com/video/{}'.format(raw_post['desc']['bvid']) + url = "https://www.bilibili.com/video/{}".format( + raw_post["desc"]["bvid"] + ) elif post_type == 4: # 纯文字 - url = 'https://t.bilibili.com/{}'.format(raw_post['desc']['dynamic_id_str']) + url = "https://t.bilibili.com/{}".format( + raw_post["desc"]["dynamic_id_str"] + ) text, pic = self._get_info(post_type, card_content) elif post_type == 5: # 转发 - url = 'https://t.bilibili.com/{}'.format(raw_post['desc']['dynamic_id_str']) - text = card_content['item']['content'] - orig_type = card_content['item']['orig_type'] - orig = json.loads(card_content['origin']) + url = "https://t.bilibili.com/{}".format(raw_post["desc"]["dynamic_id_str"]) + text = card_content["item"]["content"] + orig_type = card_content["item"]["orig_type"] + orig = json.loads(card_content["origin"]) orig_text, _ = self._get_info(self._do_get_category(orig_type), orig) - text += '\n--------------\n' + text += "\n--------------\n" text += orig_text pic = [] else: raise CategoryNotSupport(post_type) - return Post('bilibili', text=text, url=url, pics=pic, target_name=target_name) - + return Post("bilibili", text=text, url=url, pics=pic, target_name=target_name) diff --git a/src/plugins/nonebot_bison/platform/ncm_artist.py b/src/plugins/nonebot_bison/platform/ncm_artist.py index e230f65..a30072f 100644 --- a/src/plugins/nonebot_bison/platform/ncm_artist.py +++ b/src/plugins/nonebot_bison/platform/ncm_artist.py @@ -1,54 +1,58 @@ from typing import Any, Optional import httpx + from ..post import Post from ..types import RawPost, Target from .platform import NewMessage + class NcmArtist(NewMessage): categories = {} - platform_name = 'ncm-artist' + platform_name = "ncm-artist" enable_tag = False enabled = True is_common = True - schedule_type = 'interval' - schedule_kw = {'minutes': 1} + schedule_type = "interval" + schedule_kw = {"minutes": 1} name = "网易云-歌手" has_target = True async def get_target_name(self, target: Target) -> Optional[str]: async with httpx.AsyncClient() as client: res = await client.get( - "https://music.163.com/api/artist/albums/{}".format(target), - headers={'Referer': 'https://music.163.com/'} - ) + "https://music.163.com/api/artist/albums/{}".format(target), + headers={"Referer": "https://music.163.com/"}, + ) res_data = res.json() - if res_data['code'] != 200: + if res_data["code"] != 200: return - return res_data['artist']['name'] + return res_data["artist"]["name"] async def get_sub_list(self, target: Target) -> list[RawPost]: async with httpx.AsyncClient() as client: res = await client.get( - "https://music.163.com/api/artist/albums/{}".format(target), - headers={'Referer': 'https://music.163.com/'} - ) + "https://music.163.com/api/artist/albums/{}".format(target), + headers={"Referer": "https://music.163.com/"}, + ) res_data = res.json() - if res_data['code'] != 200: + if res_data["code"] != 200: return [] else: - return res_data['hotAlbums'] + return res_data["hotAlbums"] def get_id(self, post: RawPost) -> Any: - return post['id'] + return post["id"] def get_date(self, post: RawPost) -> int: - return post['publishTime'] // 1000 + return post["publishTime"] // 1000 async def parse(self, raw_post: RawPost) -> Post: - text = '新专辑发布:{}'.format(raw_post['name']) - target_name = raw_post['artist']['name'] - pics = [raw_post['picUrl']] - url = "https://music.163.com/#/album?id={}".format(raw_post['id']) - return Post('ncm-artist', text=text, url=url, pics=pics, target_name=target_name) + text = "新专辑发布:{}".format(raw_post["name"]) + target_name = raw_post["artist"]["name"] + pics = [raw_post["picUrl"]] + url = "https://music.163.com/#/album?id={}".format(raw_post["id"]) + return Post( + "ncm-artist", text=text, url=url, pics=pics, target_name=target_name + ) diff --git a/src/plugins/nonebot_bison/platform/ncm_radio.py b/src/plugins/nonebot_bison/platform/ncm_radio.py index 6fae725..20abb52 100644 --- a/src/plugins/nonebot_bison/platform/ncm_radio.py +++ b/src/plugins/nonebot_bison/platform/ncm_radio.py @@ -1,56 +1,58 @@ from typing import Any, Optional import httpx + from ..post import Post from ..types import RawPost, Target from .platform import NewMessage + class NcmRadio(NewMessage): categories = {} - platform_name = 'ncm-radio' + platform_name = "ncm-radio" enable_tag = False enabled = True is_common = False - schedule_type = 'interval' - schedule_kw = {'minutes': 10} + schedule_type = "interval" + schedule_kw = {"minutes": 10} name = "网易云-电台" has_target = True async def get_target_name(self, target: Target) -> Optional[str]: async with httpx.AsyncClient() as client: res = await client.post( - "http://music.163.com/api/dj/program/byradio", - headers={'Referer': 'https://music.163.com/'}, - data={"radioId": target, "limit": 1000, "offset": 0} - ) + "http://music.163.com/api/dj/program/byradio", + headers={"Referer": "https://music.163.com/"}, + data={"radioId": target, "limit": 1000, "offset": 0}, + ) res_data = res.json() - if res_data['code'] != 200 or res_data['programs'] == 0: + if res_data["code"] != 200 or res_data["programs"] == 0: return - return res_data['programs'][0]['radio']['name'] + return res_data["programs"][0]["radio"]["name"] async def get_sub_list(self, target: Target) -> list[RawPost]: async with httpx.AsyncClient() as client: res = await client.post( - "http://music.163.com/api/dj/program/byradio", - headers={'Referer': 'https://music.163.com/'}, - data={"radioId": target, "limit": 1000, "offset": 0} - ) + "http://music.163.com/api/dj/program/byradio", + headers={"Referer": "https://music.163.com/"}, + data={"radioId": target, "limit": 1000, "offset": 0}, + ) res_data = res.json() - if res_data['code'] != 200: + if res_data["code"] != 200: return [] else: - return res_data['programs'] + return res_data["programs"] def get_id(self, post: RawPost) -> Any: - return post['id'] + return post["id"] def get_date(self, post: RawPost) -> int: - return post['createTime'] // 1000 + return post["createTime"] // 1000 async def parse(self, raw_post: RawPost) -> Post: - text = '网易云电台更新:{}'.format(raw_post['name']) - target_name = raw_post['radio']['name'] - pics = [raw_post['coverUrl']] - url = "https://music.163.com/#/program/{}".format(raw_post['id']) - return Post('ncm-radio', text=text, url=url, pics=pics, target_name=target_name) + text = "网易云电台更新:{}".format(raw_post["name"]) + target_name = raw_post["radio"]["name"] + pics = [raw_post["coverUrl"]] + url = "https://music.163.com/#/program/{}".format(raw_post["id"]) + return Post("ncm-radio", text=text, url=url, pics=pics, target_name=target_name) diff --git a/src/plugins/nonebot_bison/platform/platform.py b/src/plugins/nonebot_bison/platform/platform.py index cff3ff6..1bec3d8 100644 --- a/src/plugins/nonebot_bison/platform/platform.py +++ b/src/plugins/nonebot_bison/platform/platform.py @@ -1,8 +1,8 @@ -from abc import abstractmethod, ABC +import time +from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass -import time -from typing import Any, Collection, Optional, Literal +from typing import Any, Collection, Literal, Optional import httpx from nonebot import logger @@ -17,26 +17,27 @@ class CategoryNotSupport(Exception): class RegistryMeta(type): - def __new__(cls, name, bases, namespace, **kwargs): return super().__new__(cls, name, bases, namespace) def __init__(cls, name, bases, namespace, **kwargs): - if kwargs.get('base'): + if kwargs.get("base"): # this is the base class cls.registry = [] - elif not kwargs.get('abstract'): + elif not kwargs.get("abstract"): # this is the subclass cls.registry.append(cls) super().__init__(name, bases, namespace, **kwargs) + class RegistryABCMeta(RegistryMeta, ABC): ... + class Platform(metaclass=RegistryABCMeta, base=True): - - schedule_type: Literal['date', 'interval', 'cron'] + + schedule_type: Literal["date", "interval", "cron"] schedule_kw: dict is_common: bool enabled: bool @@ -52,7 +53,9 @@ class Platform(metaclass=RegistryABCMeta, base=True): ... @abstractmethod - async def fetch_new_post(self, target: Target, users: list[UserSubInfo]) -> list[tuple[User, list[Post]]]: + async def fetch_new_post( + self, target: Target, users: list[UserSubInfo] + ) -> list[tuple[User, list[Post]]]: ... @abstractmethod @@ -67,7 +70,7 @@ class Platform(metaclass=RegistryABCMeta, base=True): super().__init__() self.reverse_category = {} for key, val in self.categories.items(): - self.reverse_category[val] = key + self.reverse_category[val] = key self.store = dict() @abstractmethod @@ -75,12 +78,14 @@ class Platform(metaclass=RegistryABCMeta, base=True): "Return Tag list of given RawPost" def get_stored_data(self, target: Target) -> Any: - return self.store.get(target) + return self.store.get(target) def set_stored_data(self, target: Target, data: Any): self.store[target] = data - async def filter_user_custom(self, raw_post_list: list[RawPost], cats: list[Category], tags: list[Tag]) -> list[RawPost]: + async def filter_user_custom( + self, raw_post_list: list[RawPost], cats: list[Category], tags: list[Tag] + ) -> list[RawPost]: res: list[RawPost] = [] for raw_post in raw_post_list: if self.categories: @@ -99,12 +104,16 @@ class Platform(metaclass=RegistryABCMeta, base=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]]]: + 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_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.do_parse(raw_post)) @@ -116,6 +125,7 @@ class Platform(metaclass=RegistryABCMeta, base=True): "Return category of given Rawpost" raise NotImplementedError() + class MessageProcess(Platform, abstract=True): "General message process fetch, parse, filter progress" @@ -127,7 +137,6 @@ class MessageProcess(Platform, abstract=True): def get_id(self, post: RawPost) -> Any: "Get post id of given RawPost" - async def do_parse(self, raw_post: RawPost) -> Post: post_id = self.get_id(raw_post) if post_id not in self.parse_cache: @@ -156,8 +165,11 @@ class MessageProcess(Platform, abstract=True): # post_id = self.get_id(raw_post) # if post_id in exists_posts_set: # continue - if (post_time := self.get_date(raw_post)) and time.time() - post_time > 2 * 60 * 60 and \ - plugin_config.bison_init_filter: + if ( + (post_time := self.get_date(raw_post)) + and time.time() - post_time > 2 * 60 * 60 + and plugin_config.bison_init_filter + ): continue try: self.get_category(raw_post) @@ -168,15 +180,18 @@ class MessageProcess(Platform, abstract=True): res.append(raw_post) return res + class NewMessage(MessageProcess, abstract=True): "Fetch a list of messages, filter the new messages, dispatch it to different users" @dataclass - class MessageStorage(): + class MessageStorage: inited: bool exists_posts: set[Any] - async def filter_common_with_diff(self, target: Target, raw_post_list: list[RawPost]) -> list[RawPost]: + async def filter_common_with_diff( + self, target: Target, raw_post_list: list[RawPost] + ) -> list[RawPost]: filtered_post = await self.filter_common(raw_post_list) store = self.get_stored_data(target) or self.MessageStorage(False, set()) res = [] @@ -185,7 +200,11 @@ class NewMessage(MessageProcess, abstract=True): for raw_post in filtered_post: post_id = self.get_id(raw_post) store.exists_posts.add(post_id) - logger.info('init {}-{} with {}'.format(self.platform_name, target, store.exists_posts)) + logger.info( + "init {}-{} with {}".format( + self.platform_name, target, store.exists_posts + ) + ) store.inited = True else: for raw_post in filtered_post: @@ -197,8 +216,9 @@ class NewMessage(MessageProcess, abstract=True): self.set_stored_data(target, store) return res - - async def fetch_new_post(self, target: Target, users: list[UserSubInfo]) -> list[tuple[User, list[Post]]]: + 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) @@ -206,17 +226,25 @@ class NewMessage(MessageProcess, abstract=True): return [] else: for post in new_posts: - logger.info('fetch new post from {} {}: {}'.format( - self.platform_name, - target if self.has_target else '-', - self.get_id(post))) + logger.info( + "fetch new post from {} {}: {}".format( + self.platform_name, + target if self.has_target else "-", + self.get_id(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)) + logger.warning( + "network connection error: {}, url: {}".format( + type(err), err.request.url + ) + ) return [] + class StatusChange(Platform, abstract=True): "Watch a status, and fire a post when status changes" @@ -232,49 +260,69 @@ class StatusChange(Platform, abstract=True): async def parse(self, raw_post: RawPost) -> Post: ... - async def fetch_new_post(self, target: Target, users: list[UserSubInfo]) -> list[tuple[User, list[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: - logger.info("status changes {} {}: {} -> {}".format( - self.platform_name, - target if self.has_target else '-', - old_status, new_status - )) + logger.info( + "status changes {} {}: {} -> {}".format( + self.platform_name, + target if self.has_target else "-", + old_status, + new_status, + ) + ) 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)) + logger.warning( + "network connection error: {}, url: {}".format( + type(err), err.request.url + ) + ) return [] + class SimplePost(MessageProcess, abstract=True): "Fetch a list of messages, dispatch it to different users" - async def fetch_new_post(self, target: Target, users: list[UserSubInfo]) -> list[tuple[User, list[Post]]]: + async def fetch_new_post( + self, target: Target, users: list[UserSubInfo] + ) -> list[tuple[User, list[Post]]]: try: new_posts = await self.get_sub_list(target) if not new_posts: return [] else: for post in new_posts: - logger.info('fetch new post from {} {}: {}'.format( - self.platform_name, - target if self.has_target else '-', - self.get_id(post))) + logger.info( + "fetch new post from {} {}: {}".format( + self.platform_name, + target if self.has_target else "-", + self.get_id(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)) + logger.warning( + "network connection error: {}, url: {}".format( + type(err), err.request.url + ) + ) return [] + class NoTargetGroup(Platform, abstract=True): enable_tag = False - DUMMY_STR = '_DUMMY' + DUMMY_STR = "_DUMMY" enabled = True has_target = False @@ -287,24 +335,35 @@ class NoTargetGroup(Platform, abstract=True): self.schedule_kw = platform_list[0].schedule_kw for platform in platform_list: if platform.has_target: - raise RuntimeError('Platform {} should have no target'.format(platform.name)) + raise RuntimeError( + "Platform {} should have no target".format(platform.name) + ) if name == self.DUMMY_STR: name = platform.name elif name != platform.name: - raise RuntimeError('Platform name for {} not fit'.format(self.platform_name)) + raise RuntimeError( + "Platform name for {} not fit".format(self.platform_name) + ) platform_category_key_set = set(platform.categories.keys()) if platform_category_key_set & categories_keys: - raise RuntimeError('Platform categories for {} duplicate'.format(self.platform_name)) + raise RuntimeError( + "Platform categories for {} duplicate".format(self.platform_name) + ) categories_keys |= platform_category_key_set self.categories.update(platform.categories) - if platform.schedule_kw != self.schedule_kw or platform.schedule_type != self.schedule_type: - raise RuntimeError('Platform scheduler for {} not fit'.format(self.platform_name)) + if ( + platform.schedule_kw != self.schedule_kw + or platform.schedule_type != self.schedule_type + ): + raise RuntimeError( + "Platform scheduler for {} not fit".format(self.platform_name) + ) self.name = name self.is_common = platform_list[0].is_common super().__init__() def __str__(self): - return '[' + ' '.join(map(lambda x: x.name, self.platform_list)) + ']' + return "[" + " ".join(map(lambda x: x.name, self.platform_list)) + "]" async def get_target_name(self, _): return await self.platform_list[0].get_target_name(_) @@ -316,4 +375,3 @@ class NoTargetGroup(Platform, abstract=True): for user, posts in platform_res: res[user].extend(posts) return [[key, val] for key, val in res.items()] - diff --git a/src/plugins/nonebot_bison/platform/rss.py b/src/plugins/nonebot_bison/platform/rss.py index 4cc18cc..330d93d 100644 --- a/src/plugins/nonebot_bison/platform/rss.py +++ b/src/plugins/nonebot_bison/platform/rss.py @@ -1,31 +1,32 @@ import calendar from typing import Any, Optional -from bs4 import BeautifulSoup as bs import feedparser import httpx +from bs4 import BeautifulSoup as bs from ..post import Post from ..types import RawPost, Target from .platform import NewMessage + class Rss(NewMessage): categories = {} enable_tag = False - platform_name = 'rss' + platform_name = "rss" name = "Rss" enabled = True is_common = True - schedule_type = 'interval' - schedule_kw = {'seconds': 30} + schedule_type = "interval" + schedule_kw = {"seconds": 30} has_target = True async def get_target_name(self, target: Target) -> Optional[str]: async with httpx.AsyncClient() as client: res = await client.get(target, timeout=10.0) feed = feedparser.parse(res.text) - return feed['feed']['title'] + return feed["feed"]["title"] def get_date(self, post: RawPost) -> int: return calendar.timegm(post.published_parsed) @@ -39,12 +40,18 @@ class Rss(NewMessage): feed = feedparser.parse(res) entries = feed.entries for entry in entries: - entry['_target_name'] = feed.feed.title + entry["_target_name"] = feed.feed.title return feed.entries async def parse(self, raw_post: RawPost) -> Post: - text = raw_post.get('title', '') + '\n' if raw_post.get('title') else '' - soup = bs(raw_post.description, 'html.parser') + text = raw_post.get("title", "") + "\n" if raw_post.get("title") else "" + soup = bs(raw_post.description, "html.parser") text += soup.text.strip() - pics = list(map(lambda x: x.attrs['src'], soup('img'))) - return Post('rss', text=text, url=raw_post.link, pics=pics, target_name=raw_post['_target_name']) + pics = list(map(lambda x: x.attrs["src"], soup("img"))) + return Post( + "rss", + text=text, + url=raw_post.link, + pics=pics, + target_name=raw_post["_target_name"], + ) diff --git a/src/plugins/nonebot_bison/platform/wechat.py b/src/plugins/nonebot_bison/platform/wechat.py index 7c04306..d5f5487 100644 --- a/src/plugins/nonebot_bison/platform/wechat.py +++ b/src/plugins/nonebot_bison/platform/wechat.py @@ -1,14 +1,15 @@ -from datetime import datetime import hashlib import json import re +from datetime import datetime from typing import Any, Optional -from bs4 import BeautifulSoup as bs import httpx +from bs4 import BeautifulSoup as bs from ..post import Post from ..types import * + # from .platform import Platform @@ -75,4 +76,3 @@ from ..types import * # pics=[], # url='' # ) - diff --git a/src/plugins/nonebot_bison/platform/weibo.py b/src/plugins/nonebot_bison/platform/weibo.py index 19d8703..365e3b2 100644 --- a/src/plugins/nonebot_bison/platform/weibo.py +++ b/src/plugins/nonebot_bison/platform/weibo.py @@ -1,121 +1,152 @@ -from datetime import datetime import json import re +from datetime import datetime from typing import Any, Optional -from bs4 import BeautifulSoup as bs import httpx +from bs4 import BeautifulSoup as bs from nonebot import logger from ..post import Post from ..types import * from .platform import NewMessage + class Weibo(NewMessage): categories = { - 1: '转发', - 2: '视频', - 3: '图文', - 4: '文字', - } + 1: "转发", + 2: "视频", + 3: "图文", + 4: "文字", + } enable_tag = True - platform_name = 'weibo' - name = '新浪微博' + platform_name = "weibo" + name = "新浪微博" enabled = True is_common = True - schedule_type = 'interval' - schedule_kw = {'seconds': 3} + schedule_type = "interval" + schedule_kw = {"seconds": 3} has_target = True async def get_target_name(self, target: Target) -> Optional[str]: async with httpx.AsyncClient() as client: - param = {'containerid': '100505' + target} - res = await client.get('https://m.weibo.cn/api/container/getIndex', params=param) + param = {"containerid": "100505" + target} + res = await client.get( + "https://m.weibo.cn/api/container/getIndex", params=param + ) res_dict = json.loads(res.text) - if res_dict.get('ok') == 1: - return res_dict['data']['userInfo']['screen_name'] + if res_dict.get("ok") == 1: + return res_dict["data"]["userInfo"]["screen_name"] else: return None async def get_sub_list(self, target: Target) -> list[RawPost]: async with httpx.AsyncClient() as client: - params = { 'containerid': '107603' + target} - res = await client.get('https://m.weibo.cn/api/container/getIndex?', params=params, timeout=4.0) + params = {"containerid": "107603" + target} + res = await client.get( + "https://m.weibo.cn/api/container/getIndex?", params=params, timeout=4.0 + ) res_data = json.loads(res.text) - if not res_data['ok']: + if not res_data["ok"]: return [] - custom_filter: Callable[[RawPost], bool] = lambda d: d['card_type'] == 9 - return list(filter(custom_filter, res_data['data']['cards'])) + custom_filter: Callable[[RawPost], bool] = lambda d: d["card_type"] == 9 + return list(filter(custom_filter, res_data["data"]["cards"])) def get_id(self, post: RawPost) -> Any: - return post['mblog']['id'] + return post["mblog"]["id"] def filter_platform_custom(self, raw_post: RawPost) -> bool: - return raw_post['card_type'] == 9 + return raw_post["card_type"] == 9 def get_date(self, raw_post: RawPost) -> float: - created_time = datetime.strptime(raw_post['mblog']['created_at'], '%a %b %d %H:%M:%S %z %Y') + created_time = datetime.strptime( + raw_post["mblog"]["created_at"], "%a %b %d %H:%M:%S %z %Y" + ) return created_time.timestamp() def get_tags(self, raw_post: RawPost) -> Optional[list[Tag]]: "Return Tag list of given RawPost" - text = raw_post['mblog']['text'] - soup = bs(text, 'html.parser') - res = list(map( - lambda x: x[1:-1], - filter( - lambda s: s[0] == '#' and s[-1] == '#', - map(lambda x:x.text, soup.find_all('span', class_='surl-text')) - ) - )) - super_topic_img = soup.find('img', src=re.compile(r'timeline_card_small_super_default')) + text = raw_post["mblog"]["text"] + soup = bs(text, "html.parser") + res = list( + map( + lambda x: x[1:-1], + filter( + lambda s: s[0] == "#" and s[-1] == "#", + map(lambda x: x.text, soup.find_all("span", class_="surl-text")), + ), + ) + ) + super_topic_img = soup.find( + "img", src=re.compile(r"timeline_card_small_super_default") + ) if super_topic_img: try: - res.append(super_topic_img.parent.parent.find('span', class_='surl-text').text + '超话') + res.append( + super_topic_img.parent.parent.find("span", class_="surl-text").text + + "超话" + ) except: - logger.info('super_topic extract error: {}'.format(text)) + logger.info("super_topic extract error: {}".format(text)) return res def get_category(self, raw_post: RawPost) -> Category: - if raw_post['mblog'].get('retweeted_status'): + if raw_post["mblog"].get("retweeted_status"): return Category(1) - elif raw_post['mblog'].get('page_info') and raw_post['mblog']['page_info'].get('type') == 'video': + elif ( + raw_post["mblog"].get("page_info") + and raw_post["mblog"]["page_info"].get("type") == "video" + ): return Category(2) - elif raw_post['mblog'].get('pics'): + elif raw_post["mblog"].get("pics"): return Category(3) else: return Category(4) def _get_text(self, raw_text: str) -> str: - text = raw_text.replace('
', '\n') - return bs(text, 'html.parser').text + text = raw_text.replace("
", "\n") + return bs(text, "html.parser").text async def parse(self, raw_post: RawPost) -> Post: header = { - 'accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', - 'accept-language': 'zh-CN,zh;q=0.9', - 'authority': 'm.weibo.cn', - 'cache-control': 'max-age=0', - 'sec-fetch-dest': 'empty', - 'sec-fetch-mode': 'same-origin', - 'sec-fetch-site': 'same-origin', - 'upgrade-insecure-requests': '1', - 'user-agent': 'Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) ' - 'AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 ' - 'Mobile Safari/537.36'} - info = raw_post['mblog'] - if info['isLongText'] or info['pic_num'] > 9: + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", + "accept-language": "zh-CN,zh;q=0.9", + "authority": "m.weibo.cn", + "cache-control": "max-age=0", + "sec-fetch-dest": "empty", + "sec-fetch-mode": "same-origin", + "sec-fetch-site": "same-origin", + "upgrade-insecure-requests": "1", + "user-agent": "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) " + "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.72 " + "Mobile Safari/537.36", + } + info = raw_post["mblog"] + if info["isLongText"] or info["pic_num"] > 9: async with httpx.AsyncClient() as client: - res = await client.get('https://m.weibo.cn/detail/{}'.format(info['mid']), headers=header) + res = await client.get( + "https://m.weibo.cn/detail/{}".format(info["mid"]), headers=header + ) try: - full_json_text = re.search(r'"status": ([\s\S]+),\s+"hotScheme"', res.text).group(1) + full_json_text = re.search( + r'"status": ([\s\S]+),\s+"hotScheme"', res.text + ).group(1) info = json.loads(full_json_text) except: - logger.info('detail message error: https://m.weibo.cn/detail/{}'.format(info['mid'])) - parsed_text = self._get_text(info['text']) - pic_urls = [img['large']['url'] for img in info.get('pics', [])] - detail_url = 'https://weibo.com/{}/{}'.format(info['user']['id'], info['bid']) + logger.info( + "detail message error: https://m.weibo.cn/detail/{}".format( + info["mid"] + ) + ) + parsed_text = self._get_text(info["text"]) + pic_urls = [img["large"]["url"] for img in info.get("pics", [])] + detail_url = "https://weibo.com/{}/{}".format(info["user"]["id"], info["bid"]) # return parsed_text, detail_url, pic_urls - return Post('weibo', text=parsed_text, url=detail_url, pics=pic_urls, target_name=info['user']['screen_name']) - + return Post( + "weibo", + text=parsed_text, + url=detail_url, + pics=pic_urls, + target_name=info["user"]["screen_name"], + ) diff --git a/src/plugins/nonebot_bison/plugin_config.py b/src/plugins/nonebot_bison/plugin_config.py index e477b83..2e48b4e 100644 --- a/src/plugins/nonebot_bison/plugin_config.py +++ b/src/plugins/nonebot_bison/plugin_config.py @@ -1,23 +1,25 @@ +import warnings + +import nonebot from pydantic import BaseSettings -import warnings -import nonebot class PlugConfig(BaseSettings): bison_config_path: str = "" bison_use_pic: bool = False bison_use_local: bool = False - bison_browser: str = '' + bison_browser: str = "" bison_init_filter: bool = True bison_use_queue: bool = True - bison_outer_url: str = 'http://localhost:8080/bison/' + bison_outer_url: str = "http://localhost:8080/bison/" bison_filter_log: bool = False class Config: - extra = 'ignore' + extra = "ignore" + global_config = nonebot.get_driver().config plugin_config = PlugConfig(**global_config.dict()) if plugin_config.bison_use_local: - warnings.warn('BISON_USE_LOCAL is deprecated, please use BISON_BROWSER') + warnings.warn("BISON_USE_LOCAL is deprecated, please use BISON_BROWSER") diff --git a/src/plugins/nonebot_bison/post.py b/src/plugins/nonebot_bison/post.py index 8a77a3d..604295d 100644 --- a/src/plugins/nonebot_bison/post.py +++ b/src/plugins/nonebot_bison/post.py @@ -4,14 +4,15 @@ from functools import reduce from io import BytesIO from typing import Optional, Union -from PIL import Image import httpx from nonebot import logger from nonebot.adapters.cqhttp.message import Message, MessageSegment +from PIL import Image from .plugin_config import plugin_config from .utils import parse_text + @dataclass class Post: @@ -21,7 +22,9 @@ class Post: target_name: Optional[str] = None compress: bool = False override_use_pic: Optional[bool] = None - pics: Union[list[Union[str,bytes]], list[str], list[bytes]] = field(default_factory=list) + pics: Union[list[Union[str, bytes]], list[str], list[bytes]] = field( + default_factory=list + ) extra_msg: list[Message] = field(default_factory=list) _message: Optional[list] = None @@ -56,7 +59,7 @@ class Post: cur_img = await self._pic_url_to_image(self.pics[i]) if not self._check_image_square(cur_img.size): return - if cur_img.size[1] != images[0].size[1]: # height not equal + if cur_img.size[1] != images[0].size[1]: # height not equal return images.append(cur_img) _tmp = 0 @@ -65,6 +68,7 @@ class Post: _tmp += images[i].size[0] x_coord.append(_tmp) y_coord = [0, first_image.size[1]] + async def process_row(row: int) -> bool: if len(self.pics) < (row + 1) * 3: return False @@ -86,44 +90,48 @@ class Post: images.extend(image_row) y_coord.append(y_coord[-1] + row_first_img.size[1]) return True + if await process_row(1): - matrix = (3,2) + matrix = (3, 2) else: - matrix = (3,1) + matrix = (3, 1) if await process_row(2): - matrix = (3,3) - logger.info('trigger merge image') - target = Image.new('RGB', (x_coord[-1], y_coord[-1])) + matrix = (3, 3) + logger.info("trigger merge image") + target = Image.new("RGB", (x_coord[-1], y_coord[-1])) for y in range(matrix[1]): for x in range(matrix[0]): - target.paste(images[y * matrix[0] + x], ( - x_coord[x], y_coord[y], x_coord[x+1], y_coord[y+1] - )) + target.paste( + images[y * matrix[0] + x], + (x_coord[x], y_coord[y], x_coord[x + 1], y_coord[y + 1]), + ) target_io = BytesIO() - target.save(target_io, 'JPEG') - self.pics = self.pics[matrix[0] * matrix[1]: ] + target.save(target_io, "JPEG") + self.pics = self.pics[matrix[0] * matrix[1] :] self.pics.insert(0, target_io.getvalue()) async def generate_messages(self): if self._message is None: await self._pic_merge() msgs = [] - text = '' + text = "" if self.text: if self._use_pic(): - text += '{}'.format(self.text) + text += "{}".format(self.text) else: - text += '{}'.format(self.text if len(self.text) < 500 else self.text[:500] + '...') - text += '\n来源: {}'.format(self.target_type) + text += "{}".format( + self.text if len(self.text) < 500 else self.text[:500] + "..." + ) + text += "\n来源: {}".format(self.target_type) if self.target_name: - text += ' {}'.format(self.target_name) + text += " {}".format(self.target_name) if self._use_pic(): msgs.append(await parse_text(text)) - if not self.target_type == 'rss' and self.url: + if not self.target_type == "rss" and self.url: msgs.append(MessageSegment.text(self.url)) else: if self.url: - text += ' \n详情: {}'.format(self.url) + text += " \n详情: {}".format(self.url) msgs.append(MessageSegment.text(text)) for pic in self.pics: # if isinstance(pic, bytes): @@ -137,10 +145,17 @@ class Post: return self._message def __str__(self): - return 'type: {}\nfrom: {}\ntext: {}\nurl: {}\npic: {}'.format( - self.target_type, - self.target_name, - self.text if len(self.text) < 500 else self.text[:500] + '...', - self.url, - ', '.join(map(lambda x: 'b64img' if isinstance(x, bytes) or x.startswith('base64') else x, self.pics)) - ) + return "type: {}\nfrom: {}\ntext: {}\nurl: {}\npic: {}".format( + self.target_type, + self.target_name, + self.text if len(self.text) < 500 else self.text[:500] + "...", + self.url, + ", ".join( + map( + lambda x: "b64img" + if isinstance(x, bytes) or x.startswith("base64") + else x, + self.pics, + ) + ), + ) diff --git a/src/plugins/nonebot_bison/scheduler.py b/src/plugins/nonebot_bison/scheduler.py index 949504c..9e8eaf7 100644 --- a/src/plugins/nonebot_bison/scheduler.py +++ b/src/plugins/nonebot_bison/scheduler.py @@ -1,68 +1,91 @@ import asyncio import logging -from apscheduler.schedulers.asyncio import AsyncIOScheduler -import logging import nonebot +from apscheduler.schedulers.asyncio import AsyncIOScheduler from nonebot import get_driver, logger from nonebot.log import LoguruHandler from .config import Config from .platform import platform_manager -from .send import do_send_msgs -from .send import send_msgs -from .types import UserSubInfo from .plugin_config import plugin_config +from .send import do_send_msgs, send_msgs +from .types import UserSubInfo scheduler = AsyncIOScheduler() + @get_driver().on_startup async def _start(): scheduler.configure({"apscheduler.timezone": "Asia/Shanghai"}) scheduler.start() + # get_driver().on_startup(_start) + async def fetch_and_send(target_type: str): config = Config() target = config.get_next_target(target_type) if not target: return - logger.debug('try to fecth new posts from {}, target: {}'.format(target_type, target)) + logger.debug( + "try to fecth new posts from {}, target: {}".format(target_type, target) + ) send_user_list = config.target_user_cache[target_type][target] - send_userinfo_list = list(map( - lambda user: UserSubInfo( - user, - lambda target: config.get_sub_category(target_type, target, user.user_type, user.user), - lambda target: config.get_sub_tags(target_type, target, user.user_type, user.user) - ), send_user_list)) + send_userinfo_list = list( + map( + lambda user: UserSubInfo( + user, + lambda target: config.get_sub_category( + target_type, target, user.user_type, user.user + ), + lambda target: config.get_sub_tags( + target_type, target, user.user_type, user.user + ), + ), + send_user_list, + ) + ) bot_list = list(nonebot.get_bots().values()) bot = bot_list[0] if bot_list else None - to_send = await platform_manager[target_type].fetch_new_post(target, send_userinfo_list) + to_send = await platform_manager[target_type].fetch_new_post( + target, send_userinfo_list + ) for user, send_list in to_send: for send_post in send_list: - logger.info('send to {}: {}'.format(user, send_post)) + logger.info("send to {}: {}".format(user, send_post)) if not bot: - logger.warning('no bot connected') + logger.warning("no bot connected") else: - await send_msgs(bot, user.user, user.user_type, await send_post.generate_messages()) + await send_msgs( + bot, user.user, user.user_type, await send_post.generate_messages() + ) + for platform_name, platform in platform_manager.items(): - if platform.schedule_type in ['cron', 'interval', 'date']: - logger.info(f'start scheduler for {platform_name} with {platform.schedule_type} {platform.schedule_kw}') + if platform.schedule_type in ["cron", "interval", "date"]: + logger.info( + f"start scheduler for {platform_name} with {platform.schedule_type} {platform.schedule_kw}" + ) scheduler.add_job( - fetch_and_send, platform.schedule_type, **platform.schedule_kw, - args=(platform_name,)) + fetch_and_send, + platform.schedule_type, + **platform.schedule_kw, + args=(platform_name,), + ) + class CustomLogHandler(LoguruHandler): - def filter(self, record: logging.LogRecord): - return record.msg != ('Execution of job "%s" ' - 'skipped: maximum number of running instances reached (%d)') + return record.msg != ( + 'Execution of job "%s" ' + "skipped: maximum number of running instances reached (%d)" + ) if plugin_config.bison_use_queue: - scheduler.add_job(do_send_msgs, 'interval', seconds=0.3, coalesce=True) + scheduler.add_job(do_send_msgs, "interval", seconds=0.3, coalesce=True) aps_logger = logging.getLogger("apscheduler") aps_logger.setLevel(30) diff --git a/src/plugins/nonebot_bison/send.py b/src/plugins/nonebot_bison/send.py index 023de7b..8abeece 100644 --- a/src/plugins/nonebot_bison/send.py +++ b/src/plugins/nonebot_bison/send.py @@ -8,11 +8,13 @@ from .plugin_config import plugin_config QUEUE = [] LAST_SEND_TIME = time.time() -async def _do_send(bot: 'Bot', user: str, user_type: str, msg): - if user_type == 'group': - await bot.call_api('send_group_msg', group_id=user, message=msg) - elif user_type == 'private': - await bot.call_api('send_private_msg', user_id=user, message=msg) + +async def _do_send(bot: "Bot", user: str, user_type: str, msg): + if user_type == "group": + await bot.call_api("send_group_msg", group_id=user, message=msg) + elif user_type == "private": + await bot.call_api("send_private_msg", user_id=user, message=msg) + async def do_send_msgs(): global LAST_SEND_TIME @@ -28,10 +30,11 @@ async def do_send_msgs(): else: msg_str = str(msg) if len(msg_str) > 50: - msg_str = msg_str[:50] + '...' - logger.warning(f'send msg err {e} {msg_str}') + msg_str = msg_str[:50] + "..." + logger.warning(f"send msg err {e} {msg_str}") LAST_SEND_TIME = time.time() + async def send_msgs(bot, user, user_type, msgs): if plugin_config.bison_use_queue: for msg in msgs: @@ -39,5 +42,3 @@ async def send_msgs(bot, user, user_type, msgs): else: for msg in msgs: await _do_send(bot, user, user_type, msg) - - diff --git a/src/plugins/nonebot_bison/types.py b/src/plugins/nonebot_bison/types.py index 089b2b0..8073fa8 100644 --- a/src/plugins/nonebot_bison/types.py +++ b/src/plugins/nonebot_bison/types.py @@ -1,16 +1,18 @@ -from typing import Any, Callable, NamedTuple, NewType from dataclasses import dataclass +from typing import Any, Callable, NamedTuple, NewType + +RawPost = NewType("RawPost", Any) +Target = NewType("Target", str) +Category = NewType("Category", int) +Tag = NewType("Tag", str) -RawPost = NewType('RawPost', Any) -Target = NewType('Target', str) -Category = NewType('Category', int) -Tag = NewType('Tag', str) @dataclass(eq=True, frozen=True) class User: user: str user_type: str + class UserSubInfo(NamedTuple): user: User category_getter: Callable[[Target], list[Category]] diff --git a/tests/conftest.py b/tests/conftest.py index c2bce56..c15b4a0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,26 +1,27 @@ -import pytest -import nonebot import typing +import nonebot +import pytest + if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison -@pytest.fixture#(scope="module") + +@pytest.fixture # (scope="module") def plugin_module(tmpdir): nonebot.init(bison_config_path=str(tmpdir)) - nonebot.load_plugins('src/plugins') + nonebot.load_plugins("src/plugins") plugins = nonebot.get_loaded_plugins() - plugin = list(filter(lambda x: x.name == 'nonebot_bison', plugins))[0] + plugin = list(filter(lambda x: x.name == "nonebot_bison", plugins))[0] return plugin.module -@pytest.fixture -def dummy_user_subinfo(plugin_module: 'nonebot_bison'): - user = plugin_module.types.User('123', 'group') - return plugin_module.types.UserSubInfo( - user=user, - category_getter=lambda _: [], - tag_getter=lambda _: [] - ) +@pytest.fixture +def dummy_user_subinfo(plugin_module: "nonebot_bison"): + user = plugin_module.types.User("123", "group") + return plugin_module.types.UserSubInfo( + user=user, category_getter=lambda _: [], tag_getter=lambda _: [] + ) diff --git a/tests/platforms/test_arknights.py b/tests/platforms/test_arknights.py index 1058019..7734791 100644 --- a/tests/platforms/test_arknights.py +++ b/tests/platforms/test_arknights.py @@ -1,64 +1,93 @@ -import pytest import typing + +import pytest import respx from httpx import Response if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison -from .utils import get_json, get_file +from .utils import get_file, get_json + @pytest.fixture -def arknights(plugin_module: 'nonebot_bison'): - return plugin_module.platform.platform_manager['arknights'] +def arknights(plugin_module: "nonebot_bison"): + return plugin_module.platform.platform_manager["arknights"] -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def arknights_list_0(): - return get_json('arknights_list_0.json') + return get_json("arknights_list_0.json") -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def arknights_list_1(): - return get_json('arknights_list_1.json') + return get_json("arknights_list_1.json") -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def monster_siren_list_0(): - return get_json('monster-siren_list_0.json') + return get_json("monster-siren_list_0.json") -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def monster_siren_list_1(): - return get_json('monster-siren_list_1.json') + return get_json("monster-siren_list_1.json") + @pytest.mark.asyncio @respx.mock -async def test_fetch_new(arknights, dummy_user_subinfo, arknights_list_0, arknights_list_1, monster_siren_list_0, monster_siren_list_1): - ak_list_router = respx.get("https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/announcement.meta.json") - detail_router = respx.get("https://ak.hycdn.cn/announce/IOS/announcement/805_1640074952.html") - version_router = respx.get('https://ak-conf.hypergryph.com/config/prod/official/IOS/version') - preannouncement_router = respx.get('https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/preannouncement.meta.json') +async def test_fetch_new( + arknights, + dummy_user_subinfo, + arknights_list_0, + arknights_list_1, + monster_siren_list_0, + monster_siren_list_1, +): + ak_list_router = respx.get( + "https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/announcement.meta.json" + ) + detail_router = respx.get( + "https://ak.hycdn.cn/announce/IOS/announcement/805_1640074952.html" + ) + version_router = respx.get( + "https://ak-conf.hypergryph.com/config/prod/official/IOS/version" + ) + preannouncement_router = respx.get( + "https://ak-conf.hypergryph.com/config/prod/announce_meta/IOS/preannouncement.meta.json" + ) monster_siren_router = respx.get("https://monster-siren.hypergryph.com/api/news") ak_list_router.mock(return_value=Response(200, json=arknights_list_0)) - detail_router.mock(return_value=Response(200, text=get_file('arknights-detail-805'))) - version_router.mock(return_value=Response(200, json=get_json('arknights-version-0.json'))) - preannouncement_router.mock(return_value=Response(200, json=get_json('arknights-pre-0.json'))) + detail_router.mock( + return_value=Response(200, text=get_file("arknights-detail-805")) + ) + version_router.mock( + return_value=Response(200, json=get_json("arknights-version-0.json")) + ) + preannouncement_router.mock( + return_value=Response(200, json=get_json("arknights-pre-0.json")) + ) monster_siren_router.mock(return_value=Response(200, json=monster_siren_list_0)) - target = '' + target = "" res = await arknights.fetch_new_post(target, [dummy_user_subinfo]) - assert(ak_list_router.called) - assert(len(res) == 0) - assert(not detail_router.called) + assert ak_list_router.called + assert len(res) == 0 + assert not detail_router.called mock_data = arknights_list_1 ak_list_router.mock(return_value=Response(200, json=mock_data)) res3 = await arknights.fetch_new_post(target, [dummy_user_subinfo]) - assert(len(res3[0][1]) == 1) - assert(detail_router.called) + assert len(res3[0][1]) == 1 + assert detail_router.called post = res3[0][1][0] - assert(post.target_type == 'arknights') - assert(post.text == '') - assert(post.url == '') - assert(post.target_name == '明日方舟游戏内公告') - assert(len(post.pics) == 1) + assert post.target_type == "arknights" + assert post.text == "" + assert post.url == "" + assert post.target_name == "明日方舟游戏内公告" + assert len(post.pics) == 1 # assert(post.pics == ['https://ak-fs.hypergryph.com/announce/images/20210623/e6f49aeb9547a2278678368a43b95b07.jpg']) print(res3[0][1]) r = await post.generate_messages() diff --git a/tests/platforms/test_bilibili.py b/tests/platforms/test_bilibili.py index d04c604..4b99c83 100644 --- a/tests/platforms/test_bilibili.py +++ b/tests/platforms/test_bilibili.py @@ -1,37 +1,53 @@ -import pytest import typing + +import pytest from httpx import Response if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison from .utils import get_json -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def bing_dy_list(): - return get_json('bilibili_bing_list.json')['data']['cards'] + return get_json("bilibili_bing_list.json")["data"]["cards"] + @pytest.fixture -def bilibili(plugin_module: 'nonebot_bison'): - return plugin_module.platform.platform_manager['bilibili'] +def bilibili(plugin_module: "nonebot_bison"): + return plugin_module.platform.platform_manager["bilibili"] + @pytest.mark.asyncio async def test_video_forward(bilibili, bing_dy_list): - post = await bilibili.parse(bing_dy_list[1]) - assert(post.text == '答案揭晓:宿舍!来看看投票结果\nhttps://t.bilibili.com/568093580488553786\n--------------\n#可露希尔的秘密档案# \n11:来宿舍休息一下吧 \n档案来源:lambda:\\罗德岛内务\\秘密档案 \n发布时间:9/12 1:00 P.M. \n档案类型:可见 \n档案描述:今天请了病假在宿舍休息。很舒适。 \n提供者:赫默') + post = await bilibili.parse(bing_dy_list[1]) + assert ( + post.text + == "答案揭晓:宿舍!来看看投票结果\nhttps://t.bilibili.com/568093580488553786\n--------------\n#可露希尔的秘密档案# \n11:来宿舍休息一下吧 \n档案来源:lambda:\\罗德岛内务\\秘密档案 \n发布时间:9/12 1:00 P.M. \n档案类型:可见 \n档案描述:今天请了病假在宿舍休息。很舒适。 \n提供者:赫默" + ) + @pytest.mark.asyncio async def test_article_forward(bilibili, bing_dy_list): - post = await bilibili.parse(bing_dy_list[4]) - assert(post.text == '#明日方舟##饼学大厦#\n9.11专栏更新完毕,这还塌了实属没跟新运营对上\n后边除了周日发饼和PV没提及的中文语音,稳了\n别忘了来参加#可露希尔的秘密档案#的主题投票\nhttps://t.bilibili.com/568093580488553786?tab=2' + - '\n--------------\n' + - '【明日方舟】饼学大厦#12~14(风暴瞭望&玛莉娅·临光&红松林&感谢庆典)9.11更新 更新记录09.11更新:覆盖09.10更新;以及排期更新,猜测周一周五开活动09.10更新:以周五开活动为底,PV/公告调整位置,整体结构更新09.08更新:饼学大厦#12更新,新增一件六星商店服饰(周日发饼)09.06更新:饼学大厦整栋整栋翻新,改为9.16开主线(四日无饼!)09.05凌晨更新:10.13后的排期(两日无饼,鹰角背刺,心狠手辣)前言感谢楪筱祈ぺ的动态-哔哩哔哩 (bilibili.com) 对饼学的贡献!后续排期:9.17【风暴瞭望】、10.01【玛莉娅·临光】复刻、10.1') + post = await bilibili.parse(bing_dy_list[4]) + assert ( + post.text + == "#明日方舟##饼学大厦#\n9.11专栏更新完毕,这还塌了实属没跟新运营对上\n后边除了周日发饼和PV没提及的中文语音,稳了\n别忘了来参加#可露希尔的秘密档案#的主题投票\nhttps://t.bilibili.com/568093580488553786?tab=2" + + "\n--------------\n" + + "【明日方舟】饼学大厦#12~14(风暴瞭望&玛莉娅·临光&红松林&感谢庆典)9.11更新 更新记录09.11更新:覆盖09.10更新;以及排期更新,猜测周一周五开活动09.10更新:以周五开活动为底,PV/公告调整位置,整体结构更新09.08更新:饼学大厦#12更新,新增一件六星商店服饰(周日发饼)09.06更新:饼学大厦整栋整栋翻新,改为9.16开主线(四日无饼!)09.05凌晨更新:10.13后的排期(两日无饼,鹰角背刺,心狠手辣)前言感谢楪筱祈ぺ的动态-哔哩哔哩 (bilibili.com) 对饼学的贡献!后续排期:9.17【风暴瞭望】、10.01【玛莉娅·临光】复刻、10.1" + ) + @pytest.mark.asyncio async def test_dynamic_forward(bilibili, bing_dy_list): post = await bilibili.parse(bing_dy_list[5]) - assert(post.text == '饼组主线饼学预测——9.11版\n①今日结果\n9.11 殿堂上的游禽-星极(x,新运营实锤了)\n②后续预测\n9.12 #罗德岛相簿#+#可露希尔的秘密档案#11话\n9.13 六星先锋(执旗手)干员-琴柳\n9.14 宣传策略-空弦+家具\n9.15 轮换池(+中文语音前瞻)\n9.16 停机\n9.17 #罗德岛闲逛部#+新六星EP+EP09·风暴瞭望开启\n9.19 #罗德岛相簿#' + - '\n--------------\n' + - '#明日方舟#\n【新增服饰】\n//殿堂上的游禽 - 星极\n塞壬唱片偶像企划《闪耀阶梯》特供服饰/殿堂上的游禽。星极自费参加了这项企划,尝试着用大众能接受的方式演绎天空之上的故事。\n\n_____________\n谦逊留给观众,骄傲发自歌喉,此夜,唯我璀璨。 ') + assert ( + post.text + == "饼组主线饼学预测——9.11版\n①今日结果\n9.11 殿堂上的游禽-星极(x,新运营实锤了)\n②后续预测\n9.12 #罗德岛相簿#+#可露希尔的秘密档案#11话\n9.13 六星先锋(执旗手)干员-琴柳\n9.14 宣传策略-空弦+家具\n9.15 轮换池(+中文语音前瞻)\n9.16 停机\n9.17 #罗德岛闲逛部#+新六星EP+EP09·风暴瞭望开启\n9.19 #罗德岛相簿#" + + "\n--------------\n" + + "#明日方舟#\n【新增服饰】\n//殿堂上的游禽 - 星极\n塞壬唱片偶像企划《闪耀阶梯》特供服饰/殿堂上的游禽。星极自费参加了这项企划,尝试着用大众能接受的方式演绎天空之上的故事。\n\n_____________\n谦逊留给观众,骄傲发自歌喉,此夜,唯我璀璨。 " + ) diff --git a/tests/platforms/test_ncm_artist.py b/tests/platforms/test_ncm_artist.py index f5ed1a2..37242e5 100644 --- a/tests/platforms/test_ncm_artist.py +++ b/tests/platforms/test_ncm_artist.py @@ -1,49 +1,54 @@ -from .utils import get_json +import time +import typing + import pytest import respx -import typing -import time from httpx import Response +from .utils import get_json + if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison + @pytest.fixture -def ncm_artist(plugin_module: 'nonebot_bison'): - return plugin_module.platform.platform_manager['ncm-artist'] +def ncm_artist(plugin_module: "nonebot_bison"): + return plugin_module.platform.platform_manager["ncm-artist"] -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def ncm_artist_raw(): - return get_json('ncm_siren.json') + return get_json("ncm_siren.json") -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def ncm_artist_0(ncm_artist_raw): - return { - **ncm_artist_raw, - 'hotAlbums': ncm_artist_raw['hotAlbums'][1:] - } + return {**ncm_artist_raw, "hotAlbums": ncm_artist_raw["hotAlbums"][1:]} -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def ncm_artist_1(ncm_artist_raw: dict): res = ncm_artist_raw.copy() - res['hotAlbums'] = ncm_artist_raw['hotAlbums'][:] - res['hotAlbums'][0]['publishTime'] = int(time.time() * 1000) - return res + res["hotAlbums"] = ncm_artist_raw["hotAlbums"][:] + res["hotAlbums"][0]["publishTime"] = int(time.time() * 1000) + return res + @pytest.mark.asyncio @respx.mock async def test_fetch_new(ncm_artist, ncm_artist_0, ncm_artist_1, dummy_user_subinfo): - ncm_router = respx.get("https://music.163.com/api/artist/albums/32540734") + ncm_router = respx.get("https://music.163.com/api/artist/albums/32540734") ncm_router.mock(return_value=Response(200, json=ncm_artist_0)) - target = '32540734' + target = "32540734" res = await ncm_artist.fetch_new_post(target, [dummy_user_subinfo]) - assert(ncm_router.called) - assert(len(res) == 0) + assert ncm_router.called + assert len(res) == 0 ncm_router.mock(return_value=Response(200, json=ncm_artist_1)) res2 = await ncm_artist.fetch_new_post(target, [dummy_user_subinfo]) post = res2[0][1][0] - assert(post.target_type == 'ncm-artist') - assert(post.text == '新专辑发布:Y1K') - assert(post.url == 'https://music.163.com/#/album?id=131074504') + assert post.target_type == "ncm-artist" + assert post.text == "新专辑发布:Y1K" + assert post.url == "https://music.163.com/#/album?id=131074504" diff --git a/tests/platforms/test_ncm_radio.py b/tests/platforms/test_ncm_radio.py index d248b0f..3ce6cfe 100644 --- a/tests/platforms/test_ncm_radio.py +++ b/tests/platforms/test_ncm_radio.py @@ -1,53 +1,59 @@ +import time +import typing -from .utils import get_json import pytest import respx -import typing -import time from httpx import Response +from .utils import get_json + if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison + @pytest.fixture -def ncm_radio(plugin_module: 'nonebot_bison'): - return plugin_module.platform.platform_manager['ncm-radio'] +def ncm_radio(plugin_module: "nonebot_bison"): + return plugin_module.platform.platform_manager["ncm-radio"] -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def ncm_radio_raw(): - return get_json('ncm_radio_ark.json') + return get_json("ncm_radio_ark.json") -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def ncm_radio_0(ncm_radio_raw): - return { - **ncm_radio_raw, - 'programs': ncm_radio_raw['programs'][1:] - } + return {**ncm_radio_raw, "programs": ncm_radio_raw["programs"][1:]} -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def ncm_radio_1(ncm_radio_raw: dict): res = ncm_radio_raw.copy() - res['programs'] = ncm_radio_raw['programs'][:] - res['programs'][0]['createTime'] = int(time.time() * 1000) - return res + res["programs"] = ncm_radio_raw["programs"][:] + res["programs"][0]["createTime"] = int(time.time() * 1000) + return res + @pytest.mark.asyncio @respx.mock async def test_fetch_new(ncm_radio, ncm_radio_0, ncm_radio_1, dummy_user_subinfo): - ncm_router = respx.post("http://music.163.com/api/dj/program/byradio") + ncm_router = respx.post("http://music.163.com/api/dj/program/byradio") ncm_router.mock(return_value=Response(200, json=ncm_radio_0)) - target = '793745436' + target = "793745436" res = await ncm_radio.fetch_new_post(target, [dummy_user_subinfo]) - assert(ncm_router.called) - assert(len(res) == 0) + assert ncm_router.called + assert len(res) == 0 ncm_router.mock(return_value=Response(200, json=ncm_radio_1)) res2 = await ncm_radio.fetch_new_post(target, [dummy_user_subinfo]) post = res2[0][1][0] print(post) - assert(post.target_type == 'ncm-radio') - assert(post.text == '网易云电台更新:「松烟行动」灰齐山麓') - assert(post.url == 'https://music.163.com/#/program/2494997688') - assert(post.pics == ['http://p1.music.126.net/H5em5xUNIYXcjJhOmeaSqQ==/109951166647436789.jpg']) - assert(post.target_name == '《明日方舟》游戏原声OST') + assert post.target_type == "ncm-radio" + assert post.text == "网易云电台更新:「松烟行动」灰齐山麓" + assert post.url == "https://music.163.com/#/program/2494997688" + assert post.pics == [ + "http://p1.music.126.net/H5em5xUNIYXcjJhOmeaSqQ==/109951166647436789.jpg" + ] + assert post.target_name == "《明日方舟》游戏原声OST" diff --git a/tests/platforms/test_platform.py b/tests/platforms/test_platform.py index f708863..ae55b38 100644 --- a/tests/platforms/test_platform.py +++ b/tests/platforms/test_platform.py @@ -6,42 +6,48 @@ import pytest if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison from nonebot_bison.types import * from nonebot_bison.post import Post from time import time + now = time() passed = now - 3 * 60 * 60 raw_post_list_1 = [ - {'id': 1, 'text': 'p1', 'date': now, 'tags': ['tag1'], 'category': 1} - ] + {"id": 1, "text": "p1", "date": now, "tags": ["tag1"], "category": 1} +] raw_post_list_2 = raw_post_list_1 + [ - {'id': 2, 'text': 'p2', 'date': now, 'tags': ['tag1'], 'category': 1}, - {'id': 3, 'text': 'p3', 'date': now, 'tags': ['tag2'], 'category': 2}, - {'id': 4, 'text': 'p4', 'date': now, 'tags': ['tag2'], 'category': 3} - ] + {"id": 2, "text": "p2", "date": now, "tags": ["tag1"], "category": 1}, + {"id": 3, "text": "p3", "date": now, "tags": ["tag2"], "category": 2}, + {"id": 4, "text": "p4", "date": now, "tags": ["tag2"], "category": 3}, +] + @pytest.fixture -def dummy_user(plugin_module: 'nonebot_bison'): - user = plugin_module.types.User('123', 'group') +def dummy_user(plugin_module: "nonebot_bison"): + user = plugin_module.types.User("123", "group") return user + @pytest.fixture -def user_info_factory(plugin_module: 'nonebot_bison', dummy_user): +def user_info_factory(plugin_module: "nonebot_bison", dummy_user): def _user_info(category_getter, tag_getter): return plugin_module.types.UserSubInfo(dummy_user, category_getter, tag_getter) + return _user_info + @pytest.fixture -def mock_platform_without_cats_tags(plugin_module: 'nonebot_bison'): +def mock_platform_without_cats_tags(plugin_module: "nonebot_bison"): class MockPlatform(plugin_module.platform.platform.NewMessage): - platform_name = 'mock_platform' - name = 'Mock Platform' + platform_name = "mock_platform" + name = "Mock Platform" enabled = True is_common = True schedule_interval = 10 @@ -52,21 +58,26 @@ def mock_platform_without_cats_tags(plugin_module: 'nonebot_bison'): def __init__(self): self.sub_index = 0 super().__init__() - + @staticmethod - async def get_target_name(_: 'Target'): - return 'MockPlatform' + async def get_target_name(_: "Target"): + return "MockPlatform" - def get_id(self, post: 'RawPost') -> Any: - return post['id'] + def get_id(self, post: "RawPost") -> Any: + return post["id"] - def get_date(self, raw_post: 'RawPost') -> float: - return raw_post['date'] + def get_date(self, raw_post: "RawPost") -> float: + return raw_post["date"] - async def parse(self, raw_post: 'RawPost') -> 'Post': - return plugin_module.post.Post('mock_platform', raw_post['text'], 'http://t.tt/' + str(self.get_id(raw_post)), target_name='Mock') + async def parse(self, raw_post: "RawPost") -> "Post": + return plugin_module.post.Post( + "mock_platform", + raw_post["text"], + "http://t.tt/" + str(self.get_id(raw_post)), + target_name="Mock", + ) - async def get_sub_list(self, _: 'Target'): + async def get_sub_list(self, _: "Target"): if self.sub_index == 0: self.sub_index += 1 return raw_post_list_1 @@ -75,45 +86,52 @@ def mock_platform_without_cats_tags(plugin_module: 'nonebot_bison'): return MockPlatform() + @pytest.fixture -def mock_platform(plugin_module: 'nonebot_bison'): +def mock_platform(plugin_module: "nonebot_bison"): class MockPlatform(plugin_module.platform.platform.NewMessage): - platform_name = 'mock_platform' - name = 'Mock Platform' + platform_name = "mock_platform" + name = "Mock Platform" enabled = True is_common = True schedule_interval = 10 enable_tag = True has_target = True categories = { - 1: '转发', - 2: '视频', - } + 1: "转发", + 2: "视频", + } + def __init__(self): self.sub_index = 0 super().__init__() - + @staticmethod - async def get_target_name(_: 'Target'): - return 'MockPlatform' + async def get_target_name(_: "Target"): + return "MockPlatform" - def get_id(self, post: 'RawPost') -> Any: - return post['id'] + def get_id(self, post: "RawPost") -> Any: + return post["id"] - def get_date(self, raw_post: 'RawPost') -> float: - return raw_post['date'] + def get_date(self, raw_post: "RawPost") -> float: + return raw_post["date"] - def get_tags(self, raw_post: 'RawPost') -> list['Tag']: - return raw_post['tags'] + def get_tags(self, raw_post: "RawPost") -> list["Tag"]: + return raw_post["tags"] - def get_category(self, raw_post: 'RawPost') -> 'Category': - return raw_post['category'] + def get_category(self, raw_post: "RawPost") -> "Category": + return raw_post["category"] - async def parse(self, raw_post: 'RawPost') -> 'Post': - return plugin_module.post.Post('mock_platform', raw_post['text'], 'http://t.tt/' + str(self.get_id(raw_post)), target_name='Mock') + async def parse(self, raw_post: "RawPost") -> "Post": + return plugin_module.post.Post( + "mock_platform", + raw_post["text"], + "http://t.tt/" + str(self.get_id(raw_post)), + target_name="Mock", + ) - async def get_sub_list(self, _: 'Target'): + async def get_sub_list(self, _: "Target"): if self.sub_index == 0: self.sub_index += 1 return raw_post_list_1 @@ -122,49 +140,52 @@ def mock_platform(plugin_module: 'nonebot_bison'): return MockPlatform() + @pytest.fixture -def mock_platform_no_target(plugin_module: 'nonebot_bison'): +def mock_platform_no_target(plugin_module: "nonebot_bison"): class MockPlatform(plugin_module.platform.platform.NewMessage): - platform_name = 'mock_platform' - name = 'Mock Platform' + platform_name = "mock_platform" + name = "Mock Platform" enabled = True is_common = True - schedule_type = 'interval' - schedule_kw = {'seconds': 30} + schedule_type = "interval" + schedule_kw = {"seconds": 30} enable_tag = True has_target = False - categories = { - 1: '转发', - 2: '视频', - 3: '不支持' - } + categories = {1: "转发", 2: "视频", 3: "不支持"} + def __init__(self): self.sub_index = 0 super().__init__() - + @staticmethod - async def get_target_name(_: 'Target'): - return 'MockPlatform' + async def get_target_name(_: "Target"): + return "MockPlatform" - def get_id(self, post: 'RawPost') -> Any: - return post['id'] + def get_id(self, post: "RawPost") -> Any: + return post["id"] - def get_date(self, raw_post: 'RawPost') -> float: - return raw_post['date'] + def get_date(self, raw_post: "RawPost") -> float: + return raw_post["date"] - def get_tags(self, raw_post: 'RawPost') -> list['Tag']: - return raw_post['tags'] + def get_tags(self, raw_post: "RawPost") -> list["Tag"]: + return raw_post["tags"] - def get_category(self, raw_post: 'RawPost') -> 'Category': - if raw_post['category'] == 3: + def get_category(self, raw_post: "RawPost") -> "Category": + if raw_post["category"] == 3: raise plugin_module.platform.platform.CategoryNotSupport() - return raw_post['category'] + return raw_post["category"] - async def parse(self, raw_post: 'RawPost') -> 'Post': - return plugin_module.post.Post('mock_platform', raw_post['text'], 'http://t.tt/' + str(self.get_id(raw_post)), target_name='Mock') + async def parse(self, raw_post: "RawPost") -> "Post": + return plugin_module.post.Post( + "mock_platform", + raw_post["text"], + "http://t.tt/" + str(self.get_id(raw_post)), + target_name="Mock", + ) - async def get_sub_list(self, _: 'Target'): + async def get_sub_list(self, _: "Target"): if self.sub_index == 0: self.sub_index += 1 return raw_post_list_1 @@ -173,54 +194,61 @@ def mock_platform_no_target(plugin_module: 'nonebot_bison'): return MockPlatform() + @pytest.fixture -def mock_platform_no_target_2(plugin_module: 'nonebot_bison'): +def mock_platform_no_target_2(plugin_module: "nonebot_bison"): class MockPlatform(plugin_module.platform.platform.NewMessage): - platform_name = 'mock_platform' - name = 'Mock Platform' + platform_name = "mock_platform" + name = "Mock Platform" enabled = True - schedule_type = 'interval' - schedule_kw = {'seconds': 30} + schedule_type = "interval" + schedule_kw = {"seconds": 30} is_common = True enable_tag = True has_target = False categories = { - 4: 'leixing4', - 5: 'leixing5', - } + 4: "leixing4", + 5: "leixing5", + } + def __init__(self): self.sub_index = 0 super().__init__() - + @staticmethod - async def get_target_name(_: 'Target'): - return 'MockPlatform' + async def get_target_name(_: "Target"): + return "MockPlatform" - def get_id(self, post: 'RawPost') -> Any: - return post['id'] + def get_id(self, post: "RawPost") -> Any: + return post["id"] - def get_date(self, raw_post: 'RawPost') -> float: - return raw_post['date'] + def get_date(self, raw_post: "RawPost") -> float: + return raw_post["date"] - def get_tags(self, raw_post: 'RawPost') -> list['Tag']: - return raw_post['tags'] + def get_tags(self, raw_post: "RawPost") -> list["Tag"]: + return raw_post["tags"] - def get_category(self, raw_post: 'RawPost') -> 'Category': - return raw_post['category'] + def get_category(self, raw_post: "RawPost") -> "Category": + return raw_post["category"] - async def parse(self, raw_post: 'RawPost') -> 'Post': - return plugin_module.post.Post('mock_platform_2', raw_post['text'], 'http://t.tt/' + str(self.get_id(raw_post)), target_name='Mock') + async def parse(self, raw_post: "RawPost") -> "Post": + return plugin_module.post.Post( + "mock_platform_2", + raw_post["text"], + "http://t.tt/" + str(self.get_id(raw_post)), + target_name="Mock", + ) - async def get_sub_list(self, _: 'Target'): + async def get_sub_list(self, _: "Target"): list_1 = [ - {'id': 5, 'text': 'p5', 'date': now, 'tags': ['tag1'], 'category': 4} - ] + {"id": 5, "text": "p5", "date": now, "tags": ["tag1"], "category": 4} + ] list_2 = list_1 + [ - {'id': 6, 'text': 'p6', 'date': now, 'tags': ['tag1'], 'category': 4}, - {'id': 7, 'text': 'p7', 'date': now, 'tags': ['tag2'], 'category': 5}, - ] + {"id": 6, "text": "p6", "date": now, "tags": ["tag1"], "category": 4}, + {"id": 7, "text": "p7", "date": now, "tags": ["tag2"], "category": 5}, + ] if self.sub_index == 0: self.sub_index += 1 return list_1 @@ -229,145 +257,190 @@ def mock_platform_no_target_2(plugin_module: 'nonebot_bison'): return MockPlatform() + @pytest.fixture -def mock_status_change(plugin_module: 'nonebot_bison'): +def mock_status_change(plugin_module: "nonebot_bison"): class MockPlatform(plugin_module.platform.platform.StatusChange): - platform_name = 'mock_platform' - name = 'Mock Platform' + platform_name = "mock_platform" + name = "Mock Platform" enabled = True is_common = True enable_tag = False - schedule_type = 'interval' - schedule_kw = {'seconds': 10} + schedule_type = "interval" + schedule_kw = {"seconds": 10} has_target = False categories = { - 1: '转发', - 2: '视频', - } + 1: "转发", + 2: "视频", + } + def __init__(self): self.sub_index = 0 super().__init__() - async def get_status(self, _: 'Target'): + async def get_status(self, _: "Target"): if self.sub_index == 0: self.sub_index += 1 - return {'s': False} + return {"s": False} elif self.sub_index == 1: self.sub_index += 1 - return {'s': True} + return {"s": True} else: - return {'s': False} + return {"s": False} - def compare_status(self, target, old_status, new_status) -> list['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}] + def compare_status(self, target, old_status, new_status) -> list["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 [] - async def parse(self, raw_post) -> 'Post': - return plugin_module.post.Post('mock_status', raw_post['text'], '') + 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 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 _: [])]) - assert(len(res1) == 0) - res2 = await mock_platform_without_cats_tags.fetch_new_post('dummy', [ - user_info_factory(lambda _: [], lambda _: []), - ]) - assert(len(res2) == 1) +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 _: [])] + ) + assert len(res1) == 0 + res2 = await mock_platform_without_cats_tags.fetch_new_post( + "dummy", + [ + user_info_factory(lambda _: [], lambda _: []), + ], + ) + assert len(res2) == 1 posts_1 = res2[0][1] - assert(len(posts_1) == 3) + assert len(posts_1) == 3 id_set_1 = set(map(lambda x: x.text, posts_1)) - assert('p2' in id_set_1 and 'p3' in id_set_1 and 'p4' in id_set_1) + assert "p2" in id_set_1 and "p3" in id_set_1 and "p4" in id_set_1 + @pytest.mark.asyncio async def test_new_message_target(mock_platform, user_info_factory): - res1 = await mock_platform.fetch_new_post('dummy', [user_info_factory(lambda _: [1,2], lambda _: [])]) - assert(len(res1) == 0) - res2 = await mock_platform.fetch_new_post('dummy', [ - user_info_factory(lambda _: [1,2], lambda _: []), - user_info_factory(lambda _: [1], lambda _: []), - user_info_factory(lambda _: [1,2], lambda _: ['tag1']) - ]) - assert(len(res2) == 3) + res1 = await mock_platform.fetch_new_post( + "dummy", [user_info_factory(lambda _: [1, 2], lambda _: [])] + ) + assert len(res1) == 0 + res2 = await mock_platform.fetch_new_post( + "dummy", + [ + user_info_factory(lambda _: [1, 2], lambda _: []), + user_info_factory(lambda _: [1], lambda _: []), + user_info_factory(lambda _: [1, 2], lambda _: ["tag1"]), + ], + ) + assert len(res2) == 3 posts_1 = res2[0][1] posts_2 = res2[1][1] posts_3 = res2[2][1] - assert(len(posts_1) == 2) - assert(len(posts_2) == 1) - assert(len(posts_3) == 1) + assert len(posts_1) == 2 + assert len(posts_2) == 1 + assert len(posts_3) == 1 id_set_1 = set(map(lambda x: x.text, posts_1)) id_set_2 = set(map(lambda x: x.text, posts_2)) id_set_3 = set(map(lambda x: x.text, posts_3)) - assert('p2' in id_set_1 and 'p3' in id_set_1) - assert('p2' in id_set_2) - assert('p2' in id_set_3) + 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_new_message_no_target(mock_platform_no_target, user_info_factory): - res1 = await mock_platform_no_target.fetch_new_post('dummy', [user_info_factory(lambda _: [1,2], lambda _: [])]) - assert(len(res1) == 0) - res2 = await mock_platform_no_target.fetch_new_post('dummy', [ - user_info_factory(lambda _: [1,2], lambda _: []), - user_info_factory(lambda _: [1], lambda _: []), - user_info_factory(lambda _: [1,2], lambda _: ['tag1']) - ]) - assert(len(res2) == 3) + res1 = await mock_platform_no_target.fetch_new_post( + "dummy", [user_info_factory(lambda _: [1, 2], lambda _: [])] + ) + assert len(res1) == 0 + res2 = await mock_platform_no_target.fetch_new_post( + "dummy", + [ + user_info_factory(lambda _: [1, 2], lambda _: []), + user_info_factory(lambda _: [1], lambda _: []), + user_info_factory(lambda _: [1, 2], lambda _: ["tag1"]), + ], + ) + assert len(res2) == 3 posts_1 = res2[0][1] posts_2 = res2[1][1] posts_3 = res2[2][1] - assert(len(posts_1) == 2) - assert(len(posts_2) == 1) - assert(len(posts_3) == 1) + assert len(posts_1) == 2 + assert len(posts_2) == 1 + assert len(posts_3) == 1 id_set_1 = set(map(lambda x: x.text, posts_1)) id_set_2 = set(map(lambda x: x.text, posts_2)) id_set_3 = set(map(lambda x: x.text, posts_3)) - assert('p2' in id_set_1 and 'p3' in id_set_1) - assert('p2' in id_set_2) - assert('p2' in id_set_3) - res3 = await mock_platform_no_target.fetch_new_post('dummy', [user_info_factory(lambda _: [1,2], lambda _: [])]) - assert(len(res3) == 0) + assert "p2" in id_set_1 and "p3" in id_set_1 + assert "p2" in id_set_2 + assert "p2" in id_set_3 + res3 = await mock_platform_no_target.fetch_new_post( + "dummy", [user_info_factory(lambda _: [1, 2], lambda _: [])] + ) + assert len(res3) == 0 + @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) + 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) + 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 + @pytest.mark.asyncio -async def test_group(plugin_module: 'nonebot_bison', mock_platform_no_target, mock_platform_no_target_2, user_info_factory): - group_platform = plugin_module.platform.platform.NoTargetGroup([mock_platform_no_target, mock_platform_no_target_2]) - res1 = await group_platform.fetch_new_post('dummy', [user_info_factory(lambda _: [1,4], lambda _: [])]) - assert(len(res1) == 0) - res2 = await group_platform.fetch_new_post('dummy', [user_info_factory(lambda _: [1,4], lambda _: [])]) - assert(len(res2) == 1) +async def test_group( + plugin_module: "nonebot_bison", + mock_platform_no_target, + mock_platform_no_target_2, + user_info_factory, +): + group_platform = plugin_module.platform.platform.NoTargetGroup( + [mock_platform_no_target, mock_platform_no_target_2] + ) + res1 = await group_platform.fetch_new_post( + "dummy", [user_info_factory(lambda _: [1, 4], lambda _: [])] + ) + assert len(res1) == 0 + res2 = await group_platform.fetch_new_post( + "dummy", [user_info_factory(lambda _: [1, 4], lambda _: [])] + ) + assert len(res2) == 1 posts = res2[0][1] - assert(len(posts) == 2) + assert len(posts) == 2 id_set_2 = set(map(lambda x: x.text, posts)) - assert('p2' in id_set_2 and 'p6' in id_set_2) - res3 = await group_platform.fetch_new_post('dummy', [user_info_factory(lambda _: [1,4], lambda _: [])]) - assert(len(res3) == 0) + assert "p2" in id_set_2 and "p6" in id_set_2 + res3 = await group_platform.fetch_new_post( + "dummy", [user_info_factory(lambda _: [1, 4], lambda _: [])] + ) + assert len(res3) == 0 diff --git a/tests/platforms/test_weibo.py b/tests/platforms/test_weibo.py index 5fa9c00..68ee873 100644 --- a/tests/platforms/test_weibo.py +++ b/tests/platforms/test_weibo.py @@ -1,110 +1,131 @@ -import pytest import typing -import respx from datetime import datetime -from pytz import timezone -from httpx import Response + import feedparser +import pytest +import respx +from httpx import Response +from pytz import timezone if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison -from .utils import get_json, get_file +from .utils import get_file, get_json + @pytest.fixture -def weibo(plugin_module: 'nonebot_bison'): - return plugin_module.platform.platform_manager['weibo'] +def weibo(plugin_module: "nonebot_bison"): + return plugin_module.platform.platform_manager["weibo"] -@pytest.fixture(scope='module') + +@pytest.fixture(scope="module") def weibo_ak_list_1(): - return get_json('weibo_ak_list_1.json') + return get_json("weibo_ak_list_1.json") + @pytest.mark.asyncio async def test_get_name(weibo): - name = await weibo.get_target_name('6279793937') - assert(name == "明日方舟Arknights") + name = await weibo.get_target_name("6279793937") + assert name == "明日方舟Arknights" + @pytest.mark.asyncio @respx.mock async def test_fetch_new(weibo, dummy_user_subinfo): - ak_list_router = respx.get("https://m.weibo.cn/api/container/getIndex?containerid=1076036279793937") + ak_list_router = respx.get( + "https://m.weibo.cn/api/container/getIndex?containerid=1076036279793937" + ) detail_router = respx.get("https://m.weibo.cn/detail/4649031014551911") - ak_list_router.mock(return_value=Response(200, json=get_json('weibo_ak_list_0.json'))) - detail_router.mock(return_value=Response(200, text=get_file('weibo_detail_4649031014551911'))) - target = '6279793937' + ak_list_router.mock( + return_value=Response(200, json=get_json("weibo_ak_list_0.json")) + ) + detail_router.mock( + return_value=Response(200, text=get_file("weibo_detail_4649031014551911")) + ) + target = "6279793937" res = await weibo.fetch_new_post(target, [dummy_user_subinfo]) - assert(ak_list_router.called) - assert(len(res) == 0) - assert(not detail_router.called) - mock_data = get_json('weibo_ak_list_1.json') + assert ak_list_router.called + assert len(res) == 0 + assert not detail_router.called + mock_data = get_json("weibo_ak_list_1.json") ak_list_router.mock(return_value=Response(200, json=mock_data)) # import ipdb; ipdb.set_trace() res2 = await weibo.fetch_new_post(target, [dummy_user_subinfo]) - assert(len(res2) == 0) - mock_data['data']['cards'][1]['mblog']['created_at'] = \ - datetime.now(timezone('Asia/Shanghai')).strftime('%a %b %d %H:%M:%S %z %Y') + assert len(res2) == 0 + mock_data["data"]["cards"][1]["mblog"]["created_at"] = datetime.now( + timezone("Asia/Shanghai") + ).strftime("%a %b %d %H:%M:%S %z %Y") ak_list_router.mock(return_value=Response(200, json=mock_data)) res3 = await weibo.fetch_new_post(target, [dummy_user_subinfo]) - assert(len(res3[0][1]) == 1) - assert(not detail_router.called) + assert len(res3[0][1]) == 1 + assert not detail_router.called post = res3[0][1][0] - assert(post.target_type == 'weibo') - assert(post.text == '#明日方舟#\nSideStory「沃伦姆德的薄暮」复刻现已开启! ') - assert(post.url == 'https://weibo.com/6279793937/KkBtUx2dv') - assert(post.target_name == '明日方舟Arknights') - assert(len(post.pics) == 1) + assert post.target_type == "weibo" + assert post.text == "#明日方舟#\nSideStory「沃伦姆德的薄暮」复刻现已开启! " + assert post.url == "https://weibo.com/6279793937/KkBtUx2dv" + assert post.target_name == "明日方舟Arknights" + assert len(post.pics) == 1 + @pytest.mark.asyncio async def test_classification(weibo): - mock_data = get_json('weibo_ak_list_1.json') - tuwen = mock_data['data']['cards'][1] - retweet = mock_data['data']['cards'][3] - video = mock_data['data']['cards'][0] - mock_data_ys = get_json('weibo_ys_list_0.json') - text = mock_data_ys['data']['cards'][2] - assert(weibo.get_category(retweet) == 1) - assert(weibo.get_category(video) == 2) - assert(weibo.get_category(tuwen) == 3) - assert(weibo.get_category(text) == 4) + mock_data = get_json("weibo_ak_list_1.json") + tuwen = mock_data["data"]["cards"][1] + retweet = mock_data["data"]["cards"][3] + video = mock_data["data"]["cards"][0] + mock_data_ys = get_json("weibo_ys_list_0.json") + text = mock_data_ys["data"]["cards"][2] + assert weibo.get_category(retweet) == 1 + assert weibo.get_category(video) == 2 + assert weibo.get_category(tuwen) == 3 + assert weibo.get_category(text) == 4 + @pytest.mark.asyncio @respx.mock async def test_parse_long(weibo): detail_router = respx.get("https://m.weibo.cn/detail/4645748019299849") - detail_router.mock(return_value=Response(200, text=get_file('weibo_detail_4645748019299849'))) - raw_post = get_json('weibo_ak_list_1.json')['data']['cards'][0] + detail_router.mock( + return_value=Response(200, text=get_file("weibo_detail_4645748019299849")) + ) + raw_post = get_json("weibo_ak_list_1.json")["data"]["cards"][0] post = await weibo.parse(raw_post) - assert(not '全文' in post.text) - assert(detail_router.called) + assert not "全文" in post.text + assert detail_router.called + def test_tag(weibo, weibo_ak_list_1): - raw_post = weibo_ak_list_1['data']['cards'][0] - assert(weibo.get_tags(raw_post) == ['明日方舟', '音律联觉']) + raw_post = weibo_ak_list_1["data"]["cards"][0] + assert weibo.get_tags(raw_post) == ["明日方舟", "音律联觉"] + @pytest.mark.asyncio @pytest.mark.compare async def test_rsshub_compare(weibo): - target = '6279793937' + target = "6279793937" raw_posts = filter(weibo.filter_platform_custom, await weibo.get_sub_list(target)) posts = [] for raw_post in raw_posts: posts.append(await weibo.parse(raw_post)) url_set = set(map(lambda x: x.url, posts)) - feedres = feedparser.parse('https://rsshub.app/weibo/user/6279793937') + feedres = feedparser.parse("https://rsshub.app/weibo/user/6279793937") for entry in feedres.entries[:5]: # print(entry) - assert(entry.link in url_set) + assert entry.link in url_set + test_post = { - "mblog": { - "text": "#刚出生的小羊驼长啥样#
小羊驼三三来也[好喜欢]
小羊驼三三 ", - "bid": "KnssqeqKK" - } + "mblog": { + "text": '#刚出生的小羊驼长啥样#
小羊驼三三来也[好喜欢]
小羊驼三三 ', + "bid": "KnssqeqKK", + } } + + def test_chaohua_tag(weibo): tags = weibo.get_tags(test_post) - assert('刚出生的小羊驼长啥样' in tags) - assert('小羊驼三三超话' in tags) - + assert "刚出生的小羊驼长啥样" in tags + assert "小羊驼三三超话" in tags diff --git a/tests/platforms/utils.py b/tests/platforms/utils.py index da6d0e9..b80374c 100644 --- a/tests/platforms/utils.py +++ b/tests/platforms/utils.py @@ -1,12 +1,16 @@ -from pathlib import Path import json +from pathlib import Path + path = Path(__file__).parent + + def get_json(file_name: str): - with open(path / file_name, 'r') as f: - file_text = f.read() + with open(path / file_name, "r") as f: + file_text = f.read() return json.loads(file_text) -def get_file(file_name:str): - with open(path / file_name, 'r') as f: - file_text = f.read() + +def get_file(file_name: str): + with open(path / file_name, "r") as f: + file_text = f.read() return file_text diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 5ae7fe2..bd7a638 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -1,38 +1,47 @@ -import pytest import typing +import pytest + if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison + @pytest.fixture def config(plugin_module): plugin_module.config.start_up() return plugin_module.config.Config() -def test_create_and_get(config: 'nonebot_bison.config.Config', plugin_module: 'nonebot_bison'): + +def test_create_and_get( + config: "nonebot_bison.config.Config", plugin_module: "nonebot_bison" +): config.add_subscribe( - user='123', - user_type='group', - target='weibo_id', - target_name='weibo_name', - target_type='weibo', - cats=[], - tags=[]) - confs = config.list_subscribe('123', 'group') - assert(len(confs) == 1) - assert(config.target_user_cache['weibo']['weibo_id'] == \ - [plugin_module.types.User('123', 'group')]) - assert(confs[0]['cats'] == []) + user="123", + user_type="group", + target="weibo_id", + target_name="weibo_name", + target_type="weibo", + cats=[], + tags=[], + ) + confs = config.list_subscribe("123", "group") + assert len(confs) == 1 + assert config.target_user_cache["weibo"]["weibo_id"] == [ + plugin_module.types.User("123", "group") + ] + assert confs[0]["cats"] == [] config.update_subscribe( - user='123', - user_type='group', - target='weibo_id', - target_name='weibo_name', - target_type='weibo', - cats=['1'], - tags=[]) - confs = config.list_subscribe('123', 'group') - assert(len(confs) == 1) - assert(confs[0]['cats'] == ['1']) + user="123", + user_type="group", + target="weibo_id", + target_name="weibo_name", + target_type="weibo", + cats=["1"], + tags=[], + ) + confs = config.list_subscribe("123", "group") + assert len(confs) == 1 + assert confs[0]["cats"] == ["1"] diff --git a/tests/test_merge_pic.py b/tests/test_merge_pic.py index 6dcd2c2..2008f1f 100644 --- a/tests/test_merge_pic.py +++ b/tests/test_merge_pic.py @@ -1,75 +1,87 @@ -import pytest import typing +import pytest + if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison merge_source_9 = [ - 'https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vib7zooj30dx0dxmz5.jpg', - "https://wx4.sinaimg.cn/large/0071VPLMgy1gq0vib5oqjj30dw0dxjt2.jpg", - "https://wx2.sinaimg.cn/large/0071VPLMgy1gq0vib8bjmj30dv0dxgn7.jpg", - "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vib6pn1j30dx0dw75v.jpg", - "https://wx4.sinaimg.cn/large/0071VPLMgy1gq0vib925mj30dw0dwabb.jpg", - "https://wx2.sinaimg.cn/large/0071VPLMgy1gq0vib7ujuj30dv0dwtap.jpg", - "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vibaexnj30dx0dvq49.jpg", - "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vibehw4j30dw0dv74u.jpg", - "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vibfb5fj30dv0dvtac.jpg", - "https://wx3.sinaimg.cn/large/0071VPLMgy1gq0viexkjxj30rs3pcx6p.jpg", - "https://wx2.sinaimg.cn/large/0071VPLMgy1gq0vif6qrpj30rs4mou10.jpg", - "https://wx4.sinaimg.cn/large/0071VPLMgy1gq0vifc826j30rs4a64qs.jpg", - "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vify21lj30rsbj71ld.jpg", - ] + "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vib7zooj30dx0dxmz5.jpg", + "https://wx4.sinaimg.cn/large/0071VPLMgy1gq0vib5oqjj30dw0dxjt2.jpg", + "https://wx2.sinaimg.cn/large/0071VPLMgy1gq0vib8bjmj30dv0dxgn7.jpg", + "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vib6pn1j30dx0dw75v.jpg", + "https://wx4.sinaimg.cn/large/0071VPLMgy1gq0vib925mj30dw0dwabb.jpg", + "https://wx2.sinaimg.cn/large/0071VPLMgy1gq0vib7ujuj30dv0dwtap.jpg", + "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vibaexnj30dx0dvq49.jpg", + "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vibehw4j30dw0dv74u.jpg", + "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vibfb5fj30dv0dvtac.jpg", + "https://wx3.sinaimg.cn/large/0071VPLMgy1gq0viexkjxj30rs3pcx6p.jpg", + "https://wx2.sinaimg.cn/large/0071VPLMgy1gq0vif6qrpj30rs4mou10.jpg", + "https://wx4.sinaimg.cn/large/0071VPLMgy1gq0vifc826j30rs4a64qs.jpg", + "https://wx1.sinaimg.cn/large/0071VPLMgy1gq0vify21lj30rsbj71ld.jpg", +] merge_source_9_2 = [ - 'https://wx2.sinaimg.cn/large/0071VPLMgy1gxo0eyycd7j30dw0dd3zk.jpg', - 'https://wx1.sinaimg.cn/large/0071VPLMgy1gxo0eyx6mhj30dw0ddjs8.jpg', - 'https://wx4.sinaimg.cn/large/0071VPLMgy1gxo0eyxf2bj30dw0dddh4.jpg', - 'https://wx3.sinaimg.cn/large/0071VPLMgy1gxo0ez1h5zj30dw0efwfs.jpg', - 'https://wx3.sinaimg.cn/large/0071VPLMgy1gxo0eyyku4j30dw0ef3zm.jpg', - 'https://wx1.sinaimg.cn/large/0071VPLMgy1gxo0ez0bjhj30dw0efabs.jpg', - 'https://wx4.sinaimg.cn/large/0071VPLMgy1gxo0ezdcafj30dw0dwacb.jpg', - 'https://wx1.sinaimg.cn/large/0071VPLMgy1gxo0ezg2g3j30dw0dwq51.jpg', - 'https://wx3.sinaimg.cn/large/0071VPLMgy1gxo0ez5oloj30dw0dw0uf.jpg', - 'https://wx4.sinaimg.cn/large/0071VPLMgy1gxo0fnk6stj30rs44ne81.jpg', - 'https://wx2.sinaimg.cn/large/0071VPLMgy1gxo0fohgcoj30rs3wpe81.jpg', - 'https://wx3.sinaimg.cn/large/0071VPLMgy1gxo0fpr6chj30rs3m1b29.jpg' - ] + "https://wx2.sinaimg.cn/large/0071VPLMgy1gxo0eyycd7j30dw0dd3zk.jpg", + "https://wx1.sinaimg.cn/large/0071VPLMgy1gxo0eyx6mhj30dw0ddjs8.jpg", + "https://wx4.sinaimg.cn/large/0071VPLMgy1gxo0eyxf2bj30dw0dddh4.jpg", + "https://wx3.sinaimg.cn/large/0071VPLMgy1gxo0ez1h5zj30dw0efwfs.jpg", + "https://wx3.sinaimg.cn/large/0071VPLMgy1gxo0eyyku4j30dw0ef3zm.jpg", + "https://wx1.sinaimg.cn/large/0071VPLMgy1gxo0ez0bjhj30dw0efabs.jpg", + "https://wx4.sinaimg.cn/large/0071VPLMgy1gxo0ezdcafj30dw0dwacb.jpg", + "https://wx1.sinaimg.cn/large/0071VPLMgy1gxo0ezg2g3j30dw0dwq51.jpg", + "https://wx3.sinaimg.cn/large/0071VPLMgy1gxo0ez5oloj30dw0dw0uf.jpg", + "https://wx4.sinaimg.cn/large/0071VPLMgy1gxo0fnk6stj30rs44ne81.jpg", + "https://wx2.sinaimg.cn/large/0071VPLMgy1gxo0fohgcoj30rs3wpe81.jpg", + "https://wx3.sinaimg.cn/large/0071VPLMgy1gxo0fpr6chj30rs3m1b29.jpg", +] + @pytest.mark.asyncio -async def test_9_merge(plugin_module: 'nonebot_bison'): - post = plugin_module.post.Post('', '', '', pics=merge_source_9) - await post._pic_merge() +async def test_9_merge(plugin_module: "nonebot_bison"): + post = plugin_module.post.Post("", "", "", pics=merge_source_9) + await post._pic_merge() assert len(post.pics) == 5 await post.generate_messages() + @pytest.mark.asyncio -async def test_9_merge_2(plugin_module: 'nonebot_bison'): - post = plugin_module.post.Post('', '', '', pics=merge_source_9_2) +async def test_9_merge_2(plugin_module: "nonebot_bison"): + post = plugin_module.post.Post("", "", "", pics=merge_source_9_2) await post._pic_merge() assert len(post.pics) == 4 await post.generate_messages() + @pytest.mark.asyncio async def test_6_merge(plugin_module): - post = plugin_module.post.Post('', '', '', pics=merge_source_9[0:6]+merge_source_9[9:]) - await post._pic_merge() + post = plugin_module.post.Post( + "", "", "", pics=merge_source_9[0:6] + merge_source_9[9:] + ) + await post._pic_merge() assert len(post.pics) == 5 + @pytest.mark.asyncio async def test_3_merge(plugin_module): - post = plugin_module.post.Post('', '', '', pics=merge_source_9[0:3]+merge_source_9[9:]) - await post._pic_merge() + post = plugin_module.post.Post( + "", "", "", pics=merge_source_9[0:3] + merge_source_9[9:] + ) + await post._pic_merge() assert len(post.pics) == 5 + @pytest.mark.asyncio async def test_6_merge_only(plugin_module): - post = plugin_module.post.Post('', '', '', pics=merge_source_9[0:6]) - await post._pic_merge() + post = plugin_module.post.Post("", "", "", pics=merge_source_9[0:6]) + await post._pic_merge() assert len(post.pics) == 1 + @pytest.mark.asyncio async def test_3_merge_only(plugin_module): - post = plugin_module.post.Post('', '', '', pics=merge_source_9[0:3]) - await post._pic_merge() + post = plugin_module.post.Post("", "", "", pics=merge_source_9[0:3]) + await post._pic_merge() assert len(post.pics) == 1 diff --git a/tests/test_render.py b/tests/test_render.py index 0d180ff..edeb478 100644 --- a/tests/test_render.py +++ b/tests/test_render.py @@ -1,20 +1,25 @@ -import pytest import typing +import pytest + if typing.TYPE_CHECKING: import sys - sys.path.append('./src/plugins') + + sys.path.append("./src/plugins") import nonebot_bison + @pytest.mark.asyncio @pytest.mark.render -async def test_render(plugin_module: 'nonebot_bison'): +async def test_render(plugin_module: "nonebot_bison"): render = plugin_module.utils.Render() - res = await render.text_to_pic('''a\nbbbbbbbbbbbbbbbbbbbbbb\ncd + res = await render.text_to_pic( + """a\nbbbbbbbbbbbbbbbbbbbbbb\ncd

中文

VuePress 由两部分组成:第一部分是一个极简静态网站生成器 (opens new window),它包含由 Vue 驱动的主题系统和插件 API,另一个部分是为书写技术文档而优化的默认主题,它的诞生初衷是为了支持 Vue 及其子项目的文档需求。 每一个由 VuePress 生成的页面都带有预渲染好的 HTML,也因此具有非常好的加载性能和搜索引擎优化(SEO)。同时,一旦页面被加载,Vue 将接管这些静态内容,并将其转换成一个完整的单页应用(SPA),其他的页面则会只在用户浏览到的时候才按需加载。 -''') +""" + ) From 9055a039a83c030efd90e2033d1c35877c2815ab Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Sat, 12 Feb 2022 10:22:19 +0800 Subject: [PATCH 08/10] format md --- README.md | 75 ++++++++------- docs/README.md | 12 +-- docs/dev/README.md | 60 +++++++----- docs/usage/README.md | 218 ++++++++++++++++++++++++++----------------- 4 files changed, 215 insertions(+), 150 deletions(-) diff --git a/README.md b/README.md index 9cb8798..d3c6348 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,85 @@

Bison
通用订阅推送插件

- [![pypi](https://badgen.net/pypi/v/nonebot-bison)](https://pypi.org/project/nonebot-bison/) [![license](https://img.shields.io/github/license/felinae98/nonebot-bison)](https://github.com/felinae98/nonebot-bison/blob/main/LICENSE) [![felinae98](https://circleci.com/gh/felinae98/nonebot-bison.svg?style=shield)](https://circleci.com/gh/felinae98/nonebot-bison) [![docker](https://img.shields.io/docker/image-size/felinae98/nonebot-bison)](https://hub.docker.com/r/felinae98/nonebot-bison) [![codecov](https://codecov.io/gh/felinae98/nonebot-bison/branch/main/graph/badge.svg?token=QCFIODJOOA)](https://codecov.io/gh/felinae98/nonebot-bison) -[![qq group](https://img.shields.io/badge/QQ%E7%BE%A4-868610060-orange )](https://qm.qq.com/cgi-bin/qm/qr?k=pXYMGB_e8b6so3QTqgeV6lkKDtEeYE4f&jump_from=webapi) +[![qq group](https://img.shields.io/badge/QQ%E7%BE%A4-868610060-orange)](https://qm.qq.com/cgi-bin/qm/qr?k=pXYMGB_e8b6so3QTqgeV6lkKDtEeYE4f&jump_from=webapi) [文档](https://nonebot-bison.vercel.app)|[开发文档](https://nonebot-bison.vercel.app/dev) +
## 简介 -一款自动爬取各种站点,社交平台更新动态,并将信息推送到QQ的机器人。 -基于 [`NoneBot2`](https://github.com/nonebot/nonebot2 ) 开发(诞生于明日方舟的蹲饼活动) + +一款自动爬取各种站点,社交平台更新动态,并将信息推送到 QQ 的机器人。 +基于 [`NoneBot2`](https://github.com/nonebot/nonebot2) 开发(诞生于明日方舟的蹲饼活动) +
本项目原名原名nonebot-hk-reporter -寓意本Bot要做全世界跑的最快的搬运机器人,后因名字过于暴力改名 +寓意本 Bot 要做全世界跑的最快的搬运机器人,后因名字过于暴力改名
本项目名称来源于明日方舟角色拜松——一名龙门的信使,曾经骑自行车追上骑摩托车的德克萨斯 支持的平台: -* 微博 -* B站 -* RSS -* 明日方舟 - * 塞壬唱片新闻 - * 游戏内公告 - * 版本更新等通知 -* 网易云音乐 - * 歌手发布新专辑 - * 电台更新 +- 微博 +- Bilibili +- RSS +- 明日方舟 + - 塞壬唱片新闻 + - 游戏内公告 + - 版本更新等通知 +- 网易云音乐 + - 歌手发布新专辑 + - 电台更新 ## 功能 -* 定时爬取指定网站 -* 通过图片发送文本,防止风控 -* 使用队列限制发送频率 -* 使用网页后台管理Bot订阅 + +- 定时爬取指定网站 +- 通过图片发送文本,防止风控 +- 使用队列限制发送频率 +- 使用网页后台管理 Bot 订阅 ## 使用方法 -**!!注意,如果要使用后台管理功能请使用pypi版本或者docker版本,如果直接clone源代码 -需要按下面方式进行build** + +**!!注意,如果要使用后台管理功能请使用 pypi 版本或者 docker 版本,如果直接 clone 源代码 +需要按下面方式进行 build** + ```bash cd ./admin-frontend yarn && yarn build ``` -可以使用Docker,docker-compose,作为插件安装在nonebot中,或者直接运行 + +可以使用 Docker,docker-compose,作为插件安装在 nonebot 中,或者直接运行 参考[文档](https://nonebot-bison.vercel.app/usage/#%E4%BD%BF%E7%94%A8) ## FAQ + 1. 报错`TypeError: 'type' object is not subscriptable` - 本项目使用了Python 3.9的语法,请将Python版本升级到3.9及以上,推荐使用docker部署 -2. bot不理我 - 请确认自己是群主或者管理员,并且检查`COMMAND_START`环境变量是否设为`[""]` + 本项目使用了 Python 3.9 的语法,请将 Python 版本升级到 3.9 及以上,推荐使用 docker 部署 +2. bot 不理我 + 请确认自己是群主或者管理员,并且检查`COMMAND_START`环境变量是否设为`[""]` 3. 微博漏订阅了 - 微博更新了新的风控措施,某些含有某些关键词的微博会获取不到。 + 微博更新了新的风控措施,某些含有某些关键词的微博会获取不到。 ## 参与开发 -欢迎各种PR,参与开发本插件很简单,只需要对相应平台完成几个接口的编写就行。你只需要一点简单的爬虫知识就行。 -如果对整体框架有任何意见或者建议,欢迎issue。 +欢迎各种 PR,参与开发本插件很简单,只需要对相应平台完成几个接口的编写就行。你只需要一点简单的爬虫知识就行。 + +如果对整体框架有任何意见或者建议,欢迎 issue。 ## 鸣谢 -* [`go-cqhttp`](https://github.com/Mrs4s/go-cqhttp):简单又完善的 cqhttp 实现 -* [`NoneBot2`](https://github.com/nonebot/nonebot2):超好用的开发框架 -* [`HarukaBot`](https://github.com/SK-415/HarukaBot/): 借鉴了大体的实现思路 -* [`rsshub`](https://github.com/DIYgod/RSSHub):提供了大量的api + +- [`go-cqhttp`](https://github.com/Mrs4s/go-cqhttp):简单又完善的 cqhttp 实现 +- [`NoneBot2`](https://github.com/nonebot/nonebot2):超好用的开发框架 +- [`HarukaBot`](https://github.com/SK-415/HarukaBot/): 借鉴了大体的实现思路 +- [`rsshub`](https://github.com/DIYgod/RSSHub):提供了大量的 api ## License -MIT +MIT diff --git a/docs/README.md b/docs/README.md index d79be20..704fce1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -5,11 +5,11 @@ tagline: 本bot励志做全泰拉骑车最快的信使 actionText: 快速部署 actionLink: /usage/ features: -- title: 拓展性强 - details: 没有自己想要的网站?只要简单的爬虫知识就可以给它适配一个新的网站 -- title: 通用,强大 - details: 社交媒体?网站更新?游戏开服?只要能爬就都能推,还支持自定义过滤 -- title: 后台管理 - details: 提供后台管理页面,简单快捷修改配置 + - title: 拓展性强 + details: 没有自己想要的网站?只要简单的爬虫知识就可以给它适配一个新的网站 + - title: 通用,强大 + details: 社交媒体?网站更新?游戏开服?只要能爬就都能推,还支持自定义过滤 + - title: 后台管理 + details: 提供后台管理页面,简单快捷修改配置 footer: MIT Licensed --- diff --git a/docs/dev/README.md b/docs/dev/README.md index 5137a79..d2dc9f9 100644 --- a/docs/dev/README.md +++ b/docs/dev/README.md @@ -1,53 +1,63 @@ --- sidebar: auto --- + # 开发指南 + 本插件需要你的帮助!只需要会写简单的爬虫,就能给本插件适配新的网站。 ## 基本概念 -* `nonebot_bison.post.Post`: 可以理解为推送内容,其中包含需要发送的文字,图片,链接,平台信息等 -* `nonebot_bison.types.RawPost`: 从站点/平台中爬到的单条信息 -* `nonebot_bison.types.Target`: 目标账号,Bilibili,微博等社交媒体中的账号 -* `nonebot_bison.types.Category`: 信息分类,例如视频,动态,图文,文章等 -* `nonebot_bison.types.Tag`: 信息标签,例如微博中的超话或者hashtag + +- `nonebot_bison.post.Post`: 可以理解为推送内容,其中包含需要发送的文字,图片,链接,平台信息等 +- `nonebot_bison.types.RawPost`: 从站点/平台中爬到的单条信息 +- `nonebot_bison.types.Target`: 目标账号,Bilibili,微博等社交媒体中的账号 +- `nonebot_bison.types.Category`: 信息分类,例如视频,动态,图文,文章等 +- `nonebot_bison.types.Tag`: 信息标签,例如微博中的超话或者 hashtag ## 快速上手 + 上车!我们走 先明确需要适配的站点类型,先明确两个问题: -#### 我要发送什么样的推送 -* `nonebot_bison.platform.platform.NewMessage` 最常见的类型,每次爬虫向特定接口爬取一个消息列表, - 与之前爬取的信息对比,过滤出新的消息,再根据用户自定义的分类和标签进行过滤,最后处理消息,把 - 处理过后的消息发送给用户 - 例如:微博,Bilibili -* `nonebot_bison.platform.platform.StatusChange` 每次爬虫获取一个状态,在状态改变时发布推送 - 例如:游戏开服提醒,主播上播提醒 -* `nonebot_bison.platform.platform.SimplePost` 与`NewMessage`相似,但是不过滤新的消息 - ,每次发送全部消息 - 例如:每日榜单定时发送 -#### 这个平台是否有账号的概念 -* `nonebot_bison.platform.platform.TargetMixin` 有账号的概念 - 例如:Bilibili用户,微博用户 -* `nonebot_bison.platform.platform.NoTargetMixin` 没有账号的概念 - 例如:游戏公告,教务处公告 -现在你已经选择了两个类,现在你需要在`src/plugins/nonebot_bison/platform`下新建一个py文件, +#### 我要发送什么样的推送 + +- `nonebot_bison.platform.platform.NewMessage` 最常见的类型,每次爬虫向特定接口爬取一个消息列表, + 与之前爬取的信息对比,过滤出新的消息,再根据用户自定义的分类和标签进行过滤,最后处理消息,把 + 处理过后的消息发送给用户 + 例如:微博,Bilibili +- `nonebot_bison.platform.platform.StatusChange` 每次爬虫获取一个状态,在状态改变时发布推送 + 例如:游戏开服提醒,主播上播提醒 +- `nonebot_bison.platform.platform.SimplePost` 与`NewMessage`相似,但是不过滤新的消息 + ,每次发送全部消息 + 例如:每日榜单定时发送 + +#### 这个平台是否有账号的概念 + +- `nonebot_bison.platform.platform.TargetMixin` 有账号的概念 + 例如:Bilibili 用户,微博用户 +- `nonebot_bison.platform.platform.NoTargetMixin` 没有账号的概念 + 例如:游戏公告,教务处公告 + +现在你已经选择了两个类,现在你需要在`src/plugins/nonebot_bison/platform`下新建一个 py 文件, 在里面新建一个类,继承你刚刚选择的两个类,重载一些关键的函数,然后……就完成了,不需要修改别的东西了。 -例如要适配微博,微博有账号,并且我希望bot搬运新的消息,所以微博的类应该这样定义: +例如要适配微博,微博有账号,并且我希望 bot 搬运新的消息,所以微博的类应该这样定义: + ```python class Weibo(NewMessage, TargetMixin): ... ``` 当然我们非常希望你对自己适配的平台写一些单元测试,你可以模仿`tests/platforms/test_*.py`中的内容写 -一些单元测试。为保证多次运行测试的一致性,可以mock http的响应,测试的内容包括获取RawPost,处理成Post -,测试分类以及提取tag等,当然最好和rsshub做一个交叉验证。 +一些单元测试。为保证多次运行测试的一致性,可以 mock http 的响应,测试的内容包括获取 RawPost,处理成 Post +,测试分类以及提取 tag 等,当然最好和 rsshub 做一个交叉验证。 ::: danger -Nonebot项目使用了全异步的处理方式,所以你需要对异步,Python asyncio的机制有一定了解,当然, +Nonebot 项目使用了全异步的处理方式,所以你需要对异步,Python asyncio 的机制有一定了解,当然, 依葫芦画瓢也是足够的 ::: ## 类的方法与成员变量 + ## 方法与变量的定义 diff --git a/docs/usage/README.md b/docs/usage/README.md index 558b078..57ad323 100644 --- a/docs/usage/README.md +++ b/docs/usage/README.md @@ -1,129 +1,175 @@ --- sidebar: auto --- + # 部署和使用 -本节将教你快速部署和使用一个nonebot-bison,如果你不知道要选择哪种部署方式,推荐使用[docker-compose](#docker-compose部署-推荐) + +本节将教你快速部署和使用一个 nonebot-bison,如果你不知道要选择哪种部署方式,推荐使用[docker-compose](#docker-compose部署-推荐) ## 部署 -本项目可以作为单独的Bot使用,可以作为nonebot2的插件使用 -### 作为Bot使用 + +本项目可以作为单独的 Bot 使用,可以作为 nonebot2 的插件使用 + +### 作为 Bot 使用 + 额外提供自动同意超级用户的好友申请和同意超级用户的加群邀请的功能 -#### docker-compose部署(推荐) + +#### docker-compose 部署(推荐) + 1. 在一个新的目录中下载[docker-compose.yml](https://raw.githubusercontent.com/felinae98/nonebot-bison/main/docker-compose.yml) - 将其中的``改成自己的QQ号 - ```bash - wget https://raw.githubusercontent.com/felinae98/nonebot-bison/main/docker-compose.yml - ``` -2. 运行配置go-cqhttp - ```bash - docker-compose run go-cqhttp - ``` - 通信方式选择:`3: 反向 Websocket 通信` - 编辑`bot-data/config.yml`,更改下面字段: - ``` - account: # 账号相关 - uin: # QQ账号 - password: "" # 密码为空时使用扫码登录 + 将其中的``改成自己的 QQ 号 + ```bash + wget https://raw.githubusercontent.com/felinae98/nonebot-bison/main/docker-compose.yml + ``` +2. 运行配置 go-cqhttp - message: - post-format: array + ```bash + docker-compose run go-cqhttp + ``` - ............ + 通信方式选择:`3: 反向 Websocket 通信` + 编辑`bot-data/config.yml`,更改下面字段: - servers: - - ws-reverse: - universal: ws://nonebot:8080/cqhttp/ws # 将这个字段写为这个值 - ``` -3. 登录go-cqhttp - 再次 - ```bash - docker-compose run go-cqhttp - ``` - 参考[go-cqhttp文档](https://docs.go-cqhttp.org/faq/slider.html#%E6%96%B9%E6%A1%88a-%E8%87%AA%E8%A1%8C%E6%8A%93%E5%8C%85) - 完成登录 -4. 确定完成登录后,启动bot: - ```bash - docker-compose up -d - ``` -#### docker部署 -本项目的docker镜像为`felinae98/nonebot-bison`,可以直接pull后run进行使用, + ``` + account: # 账号相关 + uin: # QQ账号 + password: "" # 密码为空时使用扫码登录 + + message: + post-format: array + + ............ + + servers: + - ws-reverse: + universal: ws://nonebot:8080/cqhttp/ws # 将这个字段写为这个值 + ``` + +3. 登录 go-cqhttp + 再次 + ```bash + docker-compose run go-cqhttp + ``` + 参考[go-cqhttp 文档](https://docs.go-cqhttp.org/faq/slider.html#%E6%96%B9%E6%A1%88a-%E8%87%AA%E8%A1%8C%E6%8A%93%E5%8C%85) + 完成登录 +4. 确定完成登录后,启动 bot: + ```bash + docker-compose up -d + ``` + +#### docker 部署 + +本项目的 docker 镜像为`felinae98/nonebot-bison`,可以直接 pull 后 run 进行使用, 相关配置参数可以使用`-e`作为环境变量传入 + #### 直接运行(不推荐) -可以参考[nonebot的运行方法](https://v2.nonebot.dev/guide/getting-started.html) + +可以参考[nonebot 的运行方法](https://v2.nonebot.dev/guide/getting-started.html) ::: danger 直接克隆源代码需要自行编译前端,否则会出现无法使用管理后台等情况。 ::: ::: danger -本项目中使用了Python 3.9的语法,如果出现问题,请检查Python版本 +本项目中使用了 Python 3.9 的语法,如果出现问题,请检查 Python 版本 ::: -1. 首先安装poetry:[安装方法](https://python-poetry.org/docs/#installation) -2. clone本项目,在项目中`poetry install`安装依赖 -2. 安装yarn,配置yarn源(推荐) -3. 在`admin-fronted`中运行`yarn && yarn build`编译前端 -3. 编辑`.env.prod`配置各种环境变量,见[Nonebot2配置](https://v2.nonebot.dev/guide/basic-configuration.html) -4. 运行`poetry run python bot.py`启动机器人 + +1. 首先安装 poetry:[安装方法](https://python-poetry.org/docs/#installation) +2. clone 本项目,在项目中`poetry install`安装依赖 +3. 安装 yarn,配置 yarn 源(推荐) +4. 在`admin-fronted`中运行`yarn && yarn build`编译前端 +5. 编辑`.env.prod`配置各种环境变量,见[Nonebot2 配置](https://v2.nonebot.dev/guide/basic-configuration.html) +6. 运行`poetry run python bot.py`启动机器人 + ### 作为插件使用 -本部分假设大家会部署nonebot2 + +本部分假设大家会部署 nonebot2 + #### 手动安装 -1. 安装pip包`nonebot-bison` + +1. 安装 pip 包`nonebot-bison` 2. 在`bot.py`中导入插件`nonebot_bison` + ### 自动安装 + 使用`nb-cli`执行:`nb plugin install nonebot_bison` + ## 配置 -可参考[源文件](https://github.com/felinae98/nonebot-bison/blob/main/src/plugins/nonebot_bison/plugin_config.py) -* `BISON_CONFIG_PATH`: 插件存放配置文件的位置,如果不设定默认为项目目录下的`data`目录 -* `BISON_USE_PIC`: 将文字渲染成图片后进行发送,多用于规避风控 -* `BISON_BROWSER`: 本插件使用Chrome来渲染图片 - * 使用browserless提供的Chrome管理服务,设置为`ws://xxxxxxxx`,值为Chrome Endpoint(推荐) - * 使用cdp连接相关服务,设置为`wsc://xxxxxxxxx` - * 使用本地安装的Chrome,设置为`local:`,例如`local:/usr/bin/google-chrome-stable` - * 如果不进行配置,那么会在启动时候自动进行安装,在官方的docker镜像中已经安装了浏览器 -::: warning -截止发布时,本项目尚不能完全与browserless兼容,目前建议使用镜像内自带的浏览器,即 -不要配置这个变量 -::: -* `BISON_OUTER_URL`: 从外部访问服务器的地址,默认为`http://localhost:8080/bison`,如果你的插件部署 - 在服务器上,建议配置为`http://<你的服务器ip>:8080/bison` -* `BISON_FILTER_LOG`: 是否过滤来自`nonebot`的warning级以下的log,如果你的bot只运行了这个插件可以考虑 -开启,默认关 -* `BISON_USE_QUEUE`: 是否用队列的方式发送消息,降低发送频率,默认开 + +可参考[源文件](https://github.com/felinae98/nonebot-bison/blob/main/src/plugins/nonebot_bison/plugin_config.py) + +- `BISON_CONFIG_PATH`: 插件存放配置文件的位置,如果不设定默认为项目目录下的`data`目录 +- `BISON_USE_PIC`: 将文字渲染成图片后进行发送,多用于规避风控 +- `BISON_BROWSER`: 本插件使用 Chrome 来渲染图片 + - 使用 browserless 提供的 Chrome 管理服务,设置为`ws://xxxxxxxx`,值为 Chrome Endpoint(推荐) + - 使用 cdp 连接相关服务,设置为`wsc://xxxxxxxxx` + - 使用本地安装的 Chrome,设置为`local:`,例如`local:/usr/bin/google-chrome-stable` + - 如果不进行配置,那么会在启动时候自动进行安装,在官方的 docker 镜像中已经安装了浏览器 + ::: warning + 截止发布时,本项目尚不能完全与 browserless 兼容,目前建议使用镜像内自带的浏览器,即 + 不要配置这个变量 + ::: +- `BISON_OUTER_URL`: 从外部访问服务器的地址,默认为`http://localhost:8080/bison`,如果你的插件部署 + 在服务器上,建议配置为`http://<你的服务器ip>:8080/bison` +- `BISON_FILTER_LOG`: 是否过滤来自`nonebot`的 warning 级以下的 log,如果你的 bot 只运行了这个插件可以考虑 + 开启,默认关 +- `BISON_USE_QUEUE`: 是否用队列的方式发送消息,降低发送频率,默认开 + ## 使用 + ::: warning -本节假设`COMMAND_START`设置中包含`''`,如果出现bot不响应的问题,请先 +本节假设`COMMAND_START`设置中包含`''`,如果出现 bot 不响应的问题,请先 排查这个设置 ::: + ### 命令 + #### 在本群中进行配置 -所有命令都需要@bot触发 -* 添加订阅(仅管理员和群主和SUPERUSER):`添加订阅` -* 查询订阅:`查询订阅` -* 删除订阅(仅管理员和群主和SUPERUSER):`删除订阅` + +所有命令都需要@bot 触发 + +- 添加订阅(仅管理员和群主和 SUPERUSER):`添加订阅` +- 查询订阅:`查询订阅` +- 删除订阅(仅管理员和群主和 SUPERUSER):`删除订阅` + #### 私聊机器人获取后台地址 + `后台管理`,之后点击返回的链接 -如果你是superuser,那么你可以管理所有群的订阅;如果你是bot所在的群的其中部分群的管理, -你可以管理你管理的群里的订阅;如果你不是任意一个群的管理,那么bot将会报错。 +如果你是 superuser,那么你可以管理所有群的订阅;如果你是 bot 所在的群的其中部分群的管理, +你可以管理你管理的群里的订阅;如果你不是任意一个群的管理,那么 bot 将会报错。 ::: tip -可以和bot通过临时聊天触发 +可以和 bot 通过临时聊天触发 ::: ::: warning -网页的身份鉴别机制全部由bot返回的链接确定,所以这个链接并不能透露给别人。 -并且链接会过期,所以一段时间后需要重新私聊bot获取新的链接。 +网页的身份鉴别机制全部由 bot 返回的链接确定,所以这个链接并不能透露给别人。 +并且链接会过期,所以一段时间后需要重新私聊 bot 获取新的链接。 ::: -#### 私聊机器人进行配置(需要SUPERUER权限) -* 添加订阅:`管理-添加订阅` -* 查询订阅:`管理-查询订阅` -* 删除订阅:`管理-删除订阅` -### 所支持平台的uid + +#### 私聊机器人进行配置(需要 SUPERUER 权限) + +- 添加订阅:`管理-添加订阅` +- 查询订阅:`管理-查询订阅` +- 删除订阅:`管理-删除订阅` + +### 所支持平台的 uid + #### Weibo -* 对于一般用户主页`https://weibo.com/u/6441489862?xxxxxxxxxxxxxxx`,`/u/`后面的数字即为uid -* 对于有个性域名的用户如:`https://weibo.com/arknights`,需要点击左侧信息标签下“更多”,链接为`https://weibo.com/6279793937/about`,其中中间数字即为uid + +- 对于一般用户主页`https://weibo.com/u/6441489862?xxxxxxxxxxxxxxx`,`/u/`后面的数字即为 uid +- 对于有个性域名的用户如:`https://weibo.com/arknights`,需要点击左侧信息标签下“更多”,链接为`https://weibo.com/6279793937/about`,其中中间数字即为 uid + #### Bilibili -主页链接一般为`https://space.bilibili.com/161775300?xxxxxxxxxx`,数字即为uid + +主页链接一般为`https://space.bilibili.com/161775300?xxxxxxxxxx`,数字即为 uid + #### RSS -RSS链接即为uid + +RSS 链接即为 uid + #### 网易云音乐-歌手 + 在网易云网页上歌手的链接一般为`https://music.163.com/#/artist?id=32540734`,`id=` -后面的数字即为uid +后面的数字即为 uid + #### 网易云音乐-电台 + 在网易云网页上电台的链接一般为`https://music.163.com/#/djradio?id=793745436`,`id=` -后面的数字即为uid +后面的数字即为 uid From 649c1cf8f2c8ac804912a7c25a59aced38b81aa3 Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Sat, 12 Feb 2022 10:35:35 +0800 Subject: [PATCH 09/10] format frontend code --- .pre-commit-config.yaml | 2 +- admin-frontend/src/App.test.tsx | 8 +- admin-frontend/src/App.tsx | 49 ++- admin-frontend/src/api/config.ts | 47 ++- admin-frontend/src/api/utils.ts | 92 +++--- admin-frontend/src/component/addSubsModal.tsx | 301 ++++++++++-------- admin-frontend/src/component/inputTag.tsx | 157 +++++---- .../src/component/subscribeCard.tsx | 260 +++++++++------ admin-frontend/src/index.tsx | 18 +- .../src/pages/admin/configPage/index.tsx | 89 +++--- admin-frontend/src/pages/admin/index.tsx | 68 ++-- admin-frontend/src/pages/auth.tsx | 41 +-- admin-frontend/src/reportWebVitals.ts | 4 +- admin-frontend/src/setupTests.ts | 2 +- admin-frontend/src/store/globalConfSlice.ts | 41 ++- admin-frontend/src/store/groupConfigSlice.ts | 37 ++- admin-frontend/src/store/hooks.ts | 8 +- admin-frontend/src/store/index.ts | 8 +- admin-frontend/src/store/loginSlice.ts | 119 ++++--- admin-frontend/src/utils/type.ts | 66 ++-- 20 files changed, 825 insertions(+), 592 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 77e9bc6..baf2d35 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,4 +20,4 @@ repos: rev: v2.5.1 hooks: - id: prettier - types_or: [markdown] + types_or: [markdown, ts, tsx] diff --git a/admin-frontend/src/App.test.tsx b/admin-frontend/src/App.test.tsx index 2a68616..d76787e 100644 --- a/admin-frontend/src/App.test.tsx +++ b/admin-frontend/src/App.test.tsx @@ -1,8 +1,8 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; -import App from './App'; +import React from "react"; +import { render, screen } from "@testing-library/react"; +import App from "./App"; -test('renders learn react link', () => { +test("renders learn react link", () => { render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); diff --git a/admin-frontend/src/App.tsx b/admin-frontend/src/App.tsx index aacb1e7..6ab4c01 100644 --- a/admin-frontend/src/App.tsx +++ b/admin-frontend/src/App.tsx @@ -1,49 +1,46 @@ -import 'antd/dist/antd.css'; -import React, {useEffect} from 'react'; -import {useDispatch, useSelector} from 'react-redux'; -import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'; -import './App.css'; -import {Admin} from './pages/admin'; -import {Auth} from './pages/auth'; -import {getGlobalConf} from './store/globalConfSlice'; -import {useAppSelector} from './store/hooks'; -import {loadLoginState, loginSelector} from './store/loginSlice'; - +import "antd/dist/antd.css"; +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { BrowserRouter as Router, Route, Switch } from "react-router-dom"; +import "./App.css"; +import { Admin } from "./pages/admin"; +import { Auth } from "./pages/auth"; +import { getGlobalConf } from "./store/globalConfSlice"; +import { useAppSelector } from "./store/hooks"; +import { loadLoginState, loginSelector } from "./store/loginSlice"; function LoginSwitch() { - const login = useSelector(loginSelector) + const login = useSelector(loginSelector); if (login.login) { return ; } else { - return ( -
- not login -
- ) + return
not login
; } } function App() { - const dispatch = useDispatch() - const globalConf = useAppSelector(state => state.globalConf) + const dispatch = useDispatch(); + const globalConf = useAppSelector((state) => state.globalConf); useEffect(() => { dispatch(getGlobalConf()); - dispatch(loadLoginState()) + dispatch(loadLoginState()); }, [dispatch]); - return <> - { globalConf.loaded && + return ( + <> + {globalConf.loaded && ( - + - + - } - ; + )} + + ); } export default App; diff --git a/admin-frontend/src/api/config.ts b/admin-frontend/src/api/config.ts index 3286f74..d66f325 100644 --- a/admin-frontend/src/api/config.ts +++ b/admin-frontend/src/api/config.ts @@ -1,6 +1,12 @@ import axios from "axios"; -import { GlobalConf, TokenResp, SubscribeResp, TargetNameResp, SubscribeConfig } from "../utils/type"; -import { baseUrl } from './utils'; +import { + GlobalConf, + TokenResp, + SubscribeResp, + TargetNameResp, + SubscribeConfig, +} from "../utils/type"; +import { baseUrl } from "./utils"; export async function getGlobalConf(): Promise { const res = await axios.get(`${baseUrl}global_conf`); @@ -8,7 +14,9 @@ export async function getGlobalConf(): Promise { } export async function auth(token: string): Promise { - const res = await axios.get(`${baseUrl}auth`, {params: {token}}); + const res = await axios.get(`${baseUrl}auth`, { + params: { token }, + }); return res.data; } @@ -17,22 +25,39 @@ export async function getSubscribe(): Promise { return res.data; } -export async function getTargetName(platformName: string, target: string): Promise { - const res = await axios.get(`${baseUrl}target_name`, {params: {platformName, target}}); +export async function getTargetName( + platformName: string, + target: string +): Promise { + const res = await axios.get(`${baseUrl}target_name`, { + params: { platformName, target }, + }); return res.data; } export async function addSubscribe(groupNumber: string, req: SubscribeConfig) { - const res = await axios.post(`${baseUrl}subs`, req, {params: {groupNumber}}) + const res = await axios.post(`${baseUrl}subs`, req, { + params: { groupNumber }, + }); return res.data; } -export async function delSubscribe(groupNumber: string, platformName: string, target: string) { - const res = await axios.delete(`${baseUrl}subs`, {params: {groupNumber, platformName, target}}); +export async function delSubscribe( + groupNumber: string, + platformName: string, + target: string +) { + const res = await axios.delete(`${baseUrl}subs`, { + params: { groupNumber, platformName, target }, + }); return res.data; } -export async function updateSubscribe(groupNumber: string, req: SubscribeConfig) { - return axios.patch(`${baseUrl}subs`, req, {params: {groupNumber}}) - .then(res => res.data); +export async function updateSubscribe( + groupNumber: string, + req: SubscribeConfig +) { + return axios + .patch(`${baseUrl}subs`, req, { params: { groupNumber } }) + .then((res) => res.data); } diff --git a/admin-frontend/src/api/utils.ts b/admin-frontend/src/api/utils.ts index ba8a7c9..13ee025 100644 --- a/admin-frontend/src/api/utils.ts +++ b/admin-frontend/src/api/utils.ts @@ -1,51 +1,61 @@ -import axios, {AxiosError} from "axios"; -import {Store} from "src/store"; -import { clearLoginStatus } from 'src/store/loginSlice'; +import axios, { AxiosError } from "axios"; +import { Store } from "src/store"; +import { clearLoginStatus } from "src/store/loginSlice"; // import { useContext } from 'react'; // import { LoginContext } from "../utils/context"; -export const baseUrl = '/bison/api/' -let store: Store +export const baseUrl = "/bison/api/"; +let store: Store; export const injectStore = (_store: Store) => { - store = _store -} + store = _store; +}; // const loginStatus = useContext(LoginContext); -axios.interceptors.request.use(function (config) { - if (config.url && config.url.startsWith(baseUrl) && config.url !== `${baseUrl}auth` - && config.url !== `${baseUrl}global_conf`) { - const token = localStorage.getItem('token'); - if (token) { - config.headers['Authorization'] = `Bearer ${token}`; - } else { - throw new axios.Cancel('User not login'); +axios.interceptors.request.use( + function (config) { + if ( + config.url && + config.url.startsWith(baseUrl) && + config.url !== `${baseUrl}auth` && + config.url !== `${baseUrl}global_conf` + ) { + const token = localStorage.getItem("token"); + if (token) { + config.headers["Authorization"] = `Bearer ${token}`; + } else { + throw new axios.Cancel("User not login"); + } } + return config; + }, + function (error) { + return Promise.reject(error); } - return config; -}, function (error) { - return Promise.reject(error); -}); +); -axios.interceptors.response.use(function (response) { - // const data = response.data; - // const parseToMap = (item: any): any => { - // if (item instanceof Array) { - // return item.map(parseToMap); - // } else if (item instanceof Object) { - // let res = new Map(); - // for (const key of Object.keys(item)) { - // res.set(key, parseToMap(item[key])); - // } - // return res; - // } else { - // return item; - // } - // } - // response.data = parseToMap(data); - return response; -}, function(error: AxiosError) { - if(error.response && error.response.status === 401) { - store.dispatch(clearLoginStatus()); +axios.interceptors.response.use( + function (response) { + // const data = response.data; + // const parseToMap = (item: any): any => { + // if (item instanceof Array) { + // return item.map(parseToMap); + // } else if (item instanceof Object) { + // let res = new Map(); + // for (const key of Object.keys(item)) { + // res.set(key, parseToMap(item[key])); + // } + // return res; + // } else { + // return item; + // } + // } + // response.data = parseToMap(data); + return response; + }, + function (error: AxiosError) { + if (error.response && error.response.status === 401) { + store.dispatch(clearLoginStatus()); + } + return Promise.reject(error); } - return Promise.reject(error); -}); +); diff --git a/admin-frontend/src/component/addSubsModal.tsx b/admin-frontend/src/component/addSubsModal.tsx index b51113e..9b68a8f 100644 --- a/admin-frontend/src/component/addSubsModal.tsx +++ b/admin-frontend/src/component/addSubsModal.tsx @@ -1,15 +1,15 @@ -import {Form, Input, Modal, Select, Tag} from 'antd'; -import React, {useEffect, useState} from "react"; -import {useSelector} from 'react-redux'; -import {addSubscribe, getTargetName, updateSubscribe} from 'src/api/config'; -import {InputTag} from 'src/component/inputTag'; -import {platformConfSelector} from 'src/store/globalConfSlice'; -import {CategoryConfig, SubscribeConfig} from 'src/utils/type'; +import { Form, Input, Modal, Select, Tag } from "antd"; +import React, { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { addSubscribe, getTargetName, updateSubscribe } from "src/api/config"; +import { InputTag } from "src/component/inputTag"; +import { platformConfSelector } from "src/store/globalConfSlice"; +import { CategoryConfig, SubscribeConfig } from "src/utils/type"; interface InputTagCustomProp { - value?: Array, - onChange?: (value: Array) => void, - disabled?: boolean + value?: Array; + onChange?: (value: Array) => void; + disabled?: boolean; } function InputTagCustom(prop: InputTagCustomProp) { const [value, setValue] = useState(prop.value || []); @@ -18,165 +18,202 @@ function InputTagCustom(prop: InputTagCustomProp) { if (prop.onChange) { prop.onChange(newVal); } - } + }; useEffect(() => { if (prop.value) { setValue(prop.value); } - }, [prop.value]) + }, [prop.value]); return ( <> - { - prop.disabled ? 不支持标签: + {prop.disabled ? ( + 不支持标签 + ) : ( <> - {value.length === 0 && - 全部标签 - } - - - } + {value.length === 0 && 全部标签} + + + )} - ) + ); } interface AddModalProp { - showModal: boolean, - groupNumber: string, - setShowModal: (s: boolean) => void, - refresh: () => void - initVal?: SubscribeConfig + showModal: boolean; + groupNumber: string; + setShowModal: (s: boolean) => void; + refresh: () => void; + initVal?: SubscribeConfig; } export function AddModal({ - showModal, groupNumber, setShowModal, refresh, initVal + showModal, + groupNumber, + setShowModal, + refresh, + initVal, }: AddModalProp) { - const [ confirmLoading, setConfirmLoading ] = useState(false); - const platformConf = useSelector(platformConfSelector) - const [ hasTarget, setHasTarget ] = useState(false); - const [ categories, setCategories ] = useState({} as CategoryConfig); - const [ enabledTag, setEnableTag ] = useState(false); - const [ form ] = Form.useForm(); - const [ inited, setInited ] = useState(false); + const [confirmLoading, setConfirmLoading] = useState(false); + const platformConf = useSelector(platformConfSelector); + const [hasTarget, setHasTarget] = useState(false); + const [categories, setCategories] = useState({} as CategoryConfig); + const [enabledTag, setEnableTag] = useState(false); + const [form] = Form.useForm(); + const [inited, setInited] = useState(false); const changePlatformSelect = (platform: string) => { - setHasTarget(_ => platformConf[platform].hasTarget); - setCategories(_ => platformConf[platform].categories); - setEnableTag(platformConf[platform].enabledTag) - if (! platformConf[platform].hasTarget) { - getTargetName(platform, 'default') - .then(res => { - console.log(res) - form.setFieldsValue({ - targetName: res.targetName, - target: '' - }) - }) + setHasTarget((_) => platformConf[platform].hasTarget); + setCategories((_) => platformConf[platform].categories); + setEnableTag(platformConf[platform].enabledTag); + if (!platformConf[platform].hasTarget) { + getTargetName(platform, "default").then((res) => { + console.log(res); + form.setFieldsValue({ + targetName: res.targetName, + target: "", + }); + }); } else { form.setFieldsValue({ - targetName: '', - target: '' - }) + targetName: "", + target: "", + }); } - } + }; const handleSubmit = (value: any) => { - let newVal = Object.assign({}, value) - if (typeof newVal.tags !== 'object') { - newVal.tags = [] + let newVal = Object.assign({}, value); + if (typeof newVal.tags !== "object") { + newVal.tags = []; } - if (typeof newVal.cats !== 'object') { - newVal.cats = [] + if (typeof newVal.cats !== "object") { + newVal.cats = []; } - if (newVal.target === '') { - newVal.target = 'default' + if (newVal.target === "") { + newVal.target = "default"; } - if (initVal) { // patch - updateSubscribe(groupNumber, newVal) - .then(() => { - setConfirmLoading(false); - setShowModal(false); - form.resetFields(); - refresh(); - }); - } else { - addSubscribe(groupNumber, newVal) - .then(() => { + if (initVal) { + // patch + updateSubscribe(groupNumber, newVal).then(() => { setConfirmLoading(false); setShowModal(false); form.resetFields(); refresh(); - }); + }); + } else { + addSubscribe(groupNumber, newVal).then(() => { + setConfirmLoading(false); + setShowModal(false); + form.resetFields(); + refresh(); + }); } - } + }; const handleModleFinish = () => { form.submit(); setConfirmLoading(() => true); - } + }; useEffect(() => { if (initVal && !inited) { const platformName = initVal.platformName; setHasTarget(platformConf[platformName].hasTarget); setCategories(platformConf[platformName].categories); setEnableTag(platformConf[platformName].enabledTag); - setInited(true) - form.setFieldsValue(initVal) + setInited(true); + form.setFieldsValue(initVal); } - }, [initVal, form, platformConf, inited]) - return setShowModal(false)} - onOk={handleModleFinish}> -
- - - - { - try { - const res = await getTargetName(form.getFieldValue('platformName'), value); - if (res.targetName) { - form.setFieldsValue({ - targetName: res.targetName - }) - return Promise.resolve() - } else { - form.setFieldsValue({ - targetName: '' - }) - return Promise.reject("账号不正确,请重新检查账号") - } - } catch { - return Promise.reject('服务器错误,请稍后再试') - } - } - } - ]}> - - - - - - 0, message: "请至少选择一个分类进行订阅"} - ]}> - + {Object.keys(platformConf).map((platformName) => ( + + {platformConf[platformName].name} - ) - } - - - - - -
+ ))} + + + { + try { + const res = await getTargetName( + form.getFieldValue("platformName"), + value + ); + if (res.targetName) { + form.setFieldsValue({ + targetName: res.targetName, + }); + return Promise.resolve(); + } else { + form.setFieldsValue({ + targetName: "", + }); + return Promise.reject("账号不正确,请重新检查账号"); + } + } catch { + return Promise.reject("服务器错误,请稍后再试"); + } + }, + }, + ]} + > + + + + + + 0, + message: "请至少选择一个分类进行订阅", + }, + ]} + > + + + + + +
+ ); } diff --git a/admin-frontend/src/component/inputTag.tsx b/admin-frontend/src/component/inputTag.tsx index 9408a61..e633e91 100644 --- a/admin-frontend/src/component/inputTag.tsx +++ b/admin-frontend/src/component/inputTag.tsx @@ -1,122 +1,151 @@ -import {Input, Tag, Tooltip} from "antd"; -import {PresetColorType, PresetStatusColorType} from 'antd/lib/_util/colors' -import {LiteralUnion} from 'antd/lib/_util/type' -import React, {useRef, useState, useEffect} from "react"; -import { PlusOutlined } from '@ant-design/icons'; +import { Input, Tag, Tooltip } from "antd"; +import { PresetColorType, PresetStatusColorType } from "antd/lib/_util/colors"; +import { LiteralUnion } from "antd/lib/_util/type"; +import React, { useRef, useState, useEffect } from "react"; +import { PlusOutlined } from "@ant-design/icons"; interface InputTagProp { - value?: Array, - onChange?: (value: Array) => void + value?: Array; + onChange?: (value: Array) => void; color?: LiteralUnion; - addText?: string + addText?: string; } export function InputTag(prop: InputTagProp) { - const [ value, setValue ] = useState>(prop.value || []); - const [ inputVisible, setInputVisible ] = useState(false); - const [ inputValue, setInputValue ] = useState(''); - const [ editInputIndex, setEditInputIndex ] = useState(-1); - const [ editInputValue, setEditInputValue ] = useState(''); + const [value, setValue] = useState>(prop.value || []); + const [inputVisible, setInputVisible] = useState(false); + const [inputValue, setInputValue] = useState(""); + const [editInputIndex, setEditInputIndex] = useState(-1); + const [editInputValue, setEditInputValue] = useState(""); const inputRef = useRef(null as any); const editInputRef = useRef(null as any); useEffect(() => { if (prop.value) { setValue(prop.value); } - }, [prop.value]) + }, [prop.value]); useEffect(() => { if (inputVisible) { - inputRef.current.focus() + inputRef.current.focus(); } }, [inputVisible]); useEffect(() => { if (editInputIndex !== -1) { - editInputRef.current.focus(); + editInputRef.current.focus(); } }, [editInputIndex]); const handleClose = (removedTag: string) => { - const tags = value.filter(tag => tag !== removedTag); - setValue(_ => tags); + const tags = value.filter((tag) => tag !== removedTag); + setValue((_) => tags); if (prop.onChange) { prop.onChange(tags); } - } - + }; + const showInput = () => { - setInputVisible(_ => true); - } + setInputVisible((_) => true); + }; const handleInputConfirm = () => { if (inputValue && value.indexOf(inputValue) === -1) { const newVal = [...value, inputValue]; - setValue(_ => newVal); + setValue((_) => newVal); if (prop.onChange) { prop.onChange(newVal); } } - setInputVisible(_ => false); - setInputValue(_ => ''); - } + setInputVisible((_) => false); + setInputValue((_) => ""); + }; const handleEditInputChange = (e: any) => { - setEditInputValue(_ => e.target.value); - } + setEditInputValue((_) => e.target.value); + }; const handleEditInputConfirm = () => { const newTags = value.slice(); newTags[editInputIndex] = editInputValue; - setValue(_ => newTags); - if (prop.onChange) { - prop.onChange(newTags); - } - setEditInputIndex(_ => -1); - setEditInputValue(_ => ''); - } + setValue((_) => newTags); + if (prop.onChange) { + prop.onChange(newTags); + } + setEditInputIndex((_) => -1); + setEditInputValue((_) => ""); + }; const handleInputChange = (e: any) => { setInputValue(e.target.value); - } + }; return ( <> - { value.map((tag, index) => { + {value.map((tag, index) => { if (editInputIndex === index) { return ( - + + ); + } + const isLongTag = tag.length > 20; + const tagElem = ( + handleClose(tag)} + > + { + setEditInputIndex((_) => index); + setEditInputValue((_) => tag); + e.preventDefault(); + }} + > + {isLongTag ? `${tag.slice(0, 20)}...` : tag} + + ); - } - const isLongTag = tag.length > 20; - const tagElem = ( - handleClose(tag)}> - { - setEditInputIndex(_ => index); - setEditInputValue(_ => tag); - e.preventDefault(); - }}> - {isLongTag ? `${tag.slice(0, 20)}...` : tag} - - + return isLongTag ? ( + + {tagElem} + + ) : ( + tagElem ); - return isLongTag ? ( - - {tagElem} - - ) : ( tagElem ); })} {inputVisible && ( - + )} {!inputVisible && ( - - {prop.addText || "Add Tag"} + + {prop.addText || "Add Tag"} )} ); - } diff --git a/admin-frontend/src/component/subscribeCard.tsx b/admin-frontend/src/component/subscribeCard.tsx index 66d38dc..37c3ef0 100644 --- a/admin-frontend/src/component/subscribeCard.tsx +++ b/admin-frontend/src/component/subscribeCard.tsx @@ -1,108 +1,190 @@ -import {CopyOutlined, DeleteOutlined, EditOutlined} from '@ant-design/icons'; -import {Card, Col, Form, message, Popconfirm, Select, Tag, Tooltip} from 'antd'; -import Modal from 'antd/lib/modal/Modal'; -import React, {useState} from "react"; -import {useDispatch, useSelector} from 'react-redux'; -import {addSubscribe, delSubscribe} from 'src/api/config'; -import {platformConfSelector} from 'src/store/globalConfSlice'; -import {groupConfigSelector, updateGroupSubs} from 'src/store/groupConfigSlice'; -import {PlatformConfig, SubscribeConfig, SubscribeResp} from 'src/utils/type'; -import {AddModal} from './addSubsModal'; +import { CopyOutlined, DeleteOutlined, EditOutlined } from "@ant-design/icons"; +import { + Card, + Col, + Form, + message, + Popconfirm, + Select, + Tag, + Tooltip, +} from "antd"; +import Modal from "antd/lib/modal/Modal"; +import React, { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { addSubscribe, delSubscribe } from "src/api/config"; +import { platformConfSelector } from "src/store/globalConfSlice"; +import { + groupConfigSelector, + updateGroupSubs, +} from "src/store/groupConfigSlice"; +import { PlatformConfig, SubscribeConfig, SubscribeResp } from "src/utils/type"; +import { AddModal } from "./addSubsModal"; interface CopyModalProp { - setShowModal: (modalShow: boolean) => void - showModal: boolean - config: SubscribeConfig, - groups: SubscribeResp - currentGroupNumber: string - reload: () => void + setShowModal: (modalShow: boolean) => void; + showModal: boolean; + config: SubscribeConfig; + groups: SubscribeResp; + currentGroupNumber: string; + reload: () => void; } -function CopyModal({setShowModal,config, - currentGroupNumber,groups,showModal,reload}: CopyModalProp) { - const [confirmLoading, setConfirmLoading] = useState(false) - const [ selectedGroups, setSelectGroups ] = useState>([]); - const postReqs = async (selectedGroups: Array, config: SubscribeConfig) => { - for(let selectedGroup of selectedGroups) { +function CopyModal({ + setShowModal, + config, + currentGroupNumber, + groups, + showModal, + reload, +}: CopyModalProp) { + const [confirmLoading, setConfirmLoading] = useState(false); + const [selectedGroups, setSelectGroups] = useState>([]); + const postReqs = async ( + selectedGroups: Array, + config: SubscribeConfig + ) => { + for (let selectedGroup of selectedGroups) { await addSubscribe(selectedGroup, config); } - } + }; const handleOk = () => { if (selectedGroups.length === 0) { message.error("请至少选择一个目标群"); - } else{ - setConfirmLoading(true) + } else { + setConfirmLoading(true); postReqs(selectedGroups, config).then(() => { - setConfirmLoading(false) - setShowModal(false) - return reload() - }) + setConfirmLoading(false); + setShowModal(false); + return reload(); + }); } - } - return setShowModal(false)} onOk={handleOk}> - + }; + return ( + setShowModal(false)} + onOk={handleOk} + > + + ); } interface SubscribeCardProp { - groupNumber: string - config: SubscribeConfig + groupNumber: string; + config: SubscribeConfig; } -export function SubscribeCard({groupNumber, config}: SubscribeCardProp) { - const platformConfs = useSelector(platformConfSelector) - const [showModal, setShowModal] = useState(false) - const [showEditModal, setShowEditModal] = useState(false) +export function SubscribeCard({ groupNumber, config }: SubscribeCardProp) { + const platformConfs = useSelector(platformConfSelector); + const [showModal, setShowModal] = useState(false); + const [showEditModal, setShowEditModal] = useState(false); const platformConf = platformConfs[config.platformName] as PlatformConfig; const dispatcher = useDispatch(); const groupSubscribes = useSelector(groupConfigSelector); - const reload = () => dispatcher(updateGroupSubs()) - const handleDelete = (groupNumber: string, platformName: string, target: string) => () => { - delSubscribe(groupNumber, platformName, target).then(() => { - reload() - }) - } + const reload = () => dispatcher(updateGroupSubs()); + const handleDelete = + (groupNumber: string, platformName: string, target: string) => () => { + delSubscribe(groupNumber, platformName, target).then(() => { + reload(); + }); + }; return ( - - - {setShowEditModal(state => !state)}}/> - , - - {setShowModal(state => !state)}}/> - , - - - , - ]}> -
- - { platformConf.hasTarget ? config.target : 无帐号 } - - - {Object.keys(platformConf.categories).length > 0 ? - config.cats.map((catKey: number) => ({platformConf.categories[catKey]})) : - 不支持类型} - - - {platformConf.enabledTag ? config.tags.length > 0 ? config.tags.map(tag => ({tag})) : (全部标签) : - 不支持Tag} - -
-
- - - - ) + + + { + setShowEditModal((state) => !state); + }} + /> + , + + { + setShowModal((state) => !state); + }} + /> + , + + + + + , + ]} + > +
+ + {platformConf.hasTarget ? ( + config.target + ) : ( + 无帐号 + )} + + + {Object.keys(platformConf.categories).length > 0 ? ( + config.cats.map((catKey: number) => ( + + {platformConf.categories[catKey]} + + )) + ) : ( + 不支持类型 + )} + + + {platformConf.enabledTag ? ( + config.tags.length > 0 ? ( + config.tags.map((tag) => ( + + {tag} + + )) + ) : ( + 全部标签 + ) + ) : ( + 不支持Tag + )} + +
+
+ + + + ); } diff --git a/admin-frontend/src/index.tsx b/admin-frontend/src/index.tsx index 5621bf9..874357b 100644 --- a/admin-frontend/src/index.tsx +++ b/admin-frontend/src/index.tsx @@ -1,11 +1,11 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import {Provider} from 'react-redux'; -import App from './App'; -import './index.css'; -import reportWebVitals from './reportWebVitals'; -import store from './store'; -import {injectStore} from 'src/api/utils'; +import React from "react"; +import ReactDOM from "react-dom"; +import { Provider } from "react-redux"; +import App from "./App"; +import "./index.css"; +import reportWebVitals from "./reportWebVitals"; +import store from "./store"; +import { injectStore } from "src/api/utils"; injectStore(store); ReactDOM.render( @@ -14,7 +14,7 @@ ReactDOM.render( , - document.getElementById('root') + document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function diff --git a/admin-frontend/src/pages/admin/configPage/index.tsx b/admin-frontend/src/pages/admin/configPage/index.tsx index 06d676a..9552fe1 100644 --- a/admin-frontend/src/pages/admin/configPage/index.tsx +++ b/admin-frontend/src/pages/admin/configPage/index.tsx @@ -1,56 +1,73 @@ -import {Button, Collapse, Empty, Row} from 'antd'; -import React, {ReactElement, useEffect, useState} from "react"; -import {useDispatch, useSelector} from 'react-redux'; -import {AddModal} from 'src/component/addSubsModal'; -import {SubscribeCard} from 'src/component/subscribeCard'; -import {groupConfigSelector, updateGroupSubs} from 'src/store/groupConfigSlice'; +import { Button, Collapse, Empty, Row } from "antd"; +import React, { ReactElement, useEffect, useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { AddModal } from "src/component/addSubsModal"; +import { SubscribeCard } from "src/component/subscribeCard"; +import { + groupConfigSelector, + updateGroupSubs, +} from "src/store/groupConfigSlice"; interface ConfigPageProp { - tab: string + tab: string; } export function ConfigPage(prop: ConfigPageProp) { - const [ showModal, setShowModal ] = useState(false); - const [ currentAddingGroupNumber, setCurrentAddingGroupNumber ] = useState(''); + const [showModal, setShowModal] = useState(false); + const [currentAddingGroupNumber, setCurrentAddingGroupNumber] = useState(""); const configData = useSelector(groupConfigSelector); const dispatcher = useDispatch(); useEffect(() => { - dispatcher(updateGroupSubs()) + dispatcher(updateGroupSubs()); }, [prop.tab, dispatcher]); - const clickNew = (groupNumber: string) => (e: React.MouseEvent) => { - setShowModal(_ => true); - setCurrentAddingGroupNumber(groupNumber); - e.stopPropagation(); - } - + const clickNew = + (groupNumber: string) => (e: React.MouseEvent) => { + setShowModal((_) => true); + setCurrentAddingGroupNumber(groupNumber); + e.stopPropagation(); + }; + if (Object.keys(configData).length === 0) { - return + return ; } else { let groups: Array = []; for (let key of Object.keys(configData)) { let value = configData[key]; groups.push( - {`${key} - ${value.name}`} - }> - - {value.subscribes.map((subs, idx) => )} + + {`${key} - ${value.name}`} + + + } + > + + {value.subscribes.map((subs, idx) => ( + + ))} - ) + ); } return ( -
- - {groups} - - dispatcher(updateGroupSubs())} - setShowModal={(s: boolean) => setShowModal(_ => s)} /> -
- ) +
+ {groups} + dispatcher(updateGroupSubs())} + setShowModal={(s: boolean) => setShowModal((_) => s)} + /> +
+ ); } } - - diff --git a/admin-frontend/src/pages/admin/index.tsx b/admin-frontend/src/pages/admin/index.tsx index 4d860be..8fe24aa 100644 --- a/admin-frontend/src/pages/admin/index.tsx +++ b/admin-frontend/src/pages/admin/index.tsx @@ -1,37 +1,39 @@ -import {BugOutlined, SettingOutlined} from '@ant-design/icons'; -import {Layout, Menu} from 'antd'; -import React, {useState} from "react"; -import {useSelector} from 'react-redux'; -import {loginSelector} from 'src/store/loginSlice'; -import './admin.css'; -import {ConfigPage} from './configPage'; +import { BugOutlined, SettingOutlined } from "@ant-design/icons"; +import { Layout, Menu } from "antd"; +import React, { useState } from "react"; +import { useSelector } from "react-redux"; +import { loginSelector } from "src/store/loginSlice"; +import "./admin.css"; +import { ConfigPage } from "./configPage"; export function Admin() { - const login = useSelector(loginSelector) - const [ tab, changeTab ] = useState("manage"); + const login = useSelector(loginSelector); + const [tab, changeTab] = useState("manage"); return ( - - -
-
- changeTab(key)}> - }>订阅管理 - { login.type === 'admin' && - }>查看日志 - } - -
- -
- { - tab === 'manage' ? - - : null - } -
-
-
- ) + + +
+ changeTab(key)} + > + }> + 订阅管理 + + {login.type === "admin" && ( + }> + 查看日志 + + )} + +
+ +
+ {tab === "manage" ? : null} +
+
+
+ ); } - diff --git a/admin-frontend/src/pages/auth.tsx b/admin-frontend/src/pages/auth.tsx index 299c58d..c27046d 100644 --- a/admin-frontend/src/pages/auth.tsx +++ b/admin-frontend/src/pages/auth.tsx @@ -1,28 +1,31 @@ -import React, {useEffect} from "react"; -import {useDispatch, useSelector} from "react-redux"; -import {useParams} from "react-router"; -import {Redirect} from 'react-router-dom'; -import {login, loginSelector} from 'src/store/loginSlice'; +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useParams } from "react-router"; +import { Redirect } from "react-router-dom"; +import { login, loginSelector } from "src/store/loginSlice"; -interface AuthParam { - code: string +interface AuthParam { + code: string; } export function Auth() { - const { code } = useParams(); + const { code } = useParams(); const dispatch = useDispatch(); - const loginState = useSelector(loginSelector) + const loginState = useSelector(loginSelector); useEffect(() => { const loginFun = async () => { dispatch(login(code)); - } + }; loginFun(); - }, [code, dispatch]) - return <> - { loginState.login ? - : - loginState.failed ? -
登录失败,请重新获取连接
: -
Logining...
- } -; + }, [code, dispatch]); + return ( + <> + {loginState.login ? ( + + ) : loginState.failed ? ( +
登录失败,请重新获取连接
+ ) : ( +
Logining...
+ )} + + ); } diff --git a/admin-frontend/src/reportWebVitals.ts b/admin-frontend/src/reportWebVitals.ts index 49a2a16..5fa3583 100644 --- a/admin-frontend/src/reportWebVitals.ts +++ b/admin-frontend/src/reportWebVitals.ts @@ -1,8 +1,8 @@ -import { ReportHandler } from 'web-vitals'; +import { ReportHandler } from "web-vitals"; const reportWebVitals = (onPerfEntry?: ReportHandler) => { if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { + import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); diff --git a/admin-frontend/src/setupTests.ts b/admin-frontend/src/setupTests.ts index 8f2609b..1dd407a 100644 --- a/admin-frontend/src/setupTests.ts +++ b/admin-frontend/src/setupTests.ts @@ -2,4 +2,4 @@ // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; +import "@testing-library/jest-dom"; diff --git a/admin-frontend/src/store/globalConfSlice.ts b/admin-frontend/src/store/globalConfSlice.ts index 914066a..d11d52b 100644 --- a/admin-frontend/src/store/globalConfSlice.ts +++ b/admin-frontend/src/store/globalConfSlice.ts @@ -1,23 +1,31 @@ -import {CaseReducer, createAsyncThunk, createSlice, PayloadAction} from "@reduxjs/toolkit"; -import {getGlobalConf as getGlobalConfApi} from "src/api/config"; -import {GlobalConf} from "src/utils/type"; -import {RootState} from "."; - +import { + CaseReducer, + createAsyncThunk, + createSlice, + PayloadAction, +} from "@reduxjs/toolkit"; +import { getGlobalConf as getGlobalConfApi } from "src/api/config"; +import { GlobalConf } from "src/utils/type"; +import { RootState } from "."; const initialState: GlobalConf = { platformConf: {}, - loaded: false -} + loaded: false, +}; -const setGlobalConf: CaseReducer> = (_, action) => { - return {...action.payload, loaded: true} -} +const setGlobalConf: CaseReducer> = ( + _, + action +) => { + return { ...action.payload, loaded: true }; +}; export const getGlobalConf = createAsyncThunk( "globalConf/set", getGlobalConfApi, { - condition: (_, { getState }) => !(getState() as RootState).globalConf.loaded + condition: (_, { getState }) => + !(getState() as RootState).globalConf.loaded, } ); @@ -26,10 +34,11 @@ export const globalConfSlice = createSlice({ initialState, reducers: {}, extraReducers: (builder) => { - builder.addCase(getGlobalConf.fulfilled, setGlobalConf) - } -}) + builder.addCase(getGlobalConf.fulfilled, setGlobalConf); + }, +}); -export const platformConfSelector = (state: RootState) => state.globalConf.platformConf +export const platformConfSelector = (state: RootState) => + state.globalConf.platformConf; -export default globalConfSlice.reducer +export default globalConfSlice.reducer; diff --git a/admin-frontend/src/store/groupConfigSlice.ts b/admin-frontend/src/store/groupConfigSlice.ts index 3f42130..98089c8 100644 --- a/admin-frontend/src/store/groupConfigSlice.ts +++ b/admin-frontend/src/store/groupConfigSlice.ts @@ -1,27 +1,36 @@ -import {CaseReducer, createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit'; -import {SubscribeResp} from 'src/utils/type'; -import {getSubscribe} from 'src/api/config'; -import {RootState} from '.'; -const initialState: SubscribeResp = {} +import { + CaseReducer, + createAsyncThunk, + createSlice, + PayloadAction, +} from "@reduxjs/toolkit"; +import { SubscribeResp } from "src/utils/type"; +import { getSubscribe } from "src/api/config"; +import { RootState } from "."; +const initialState: SubscribeResp = {}; -const setSubs: CaseReducer> = (_, action) => { - return action.payload -} +const setSubs: CaseReducer> = ( + _, + action +) => { + return action.payload; +}; export const updateGroupSubs = createAsyncThunk( - "groupConfig/update", getSubscribe -) + "groupConfig/update", + getSubscribe +); export const groupConfigSlice = createSlice({ name: "groupConfig", initialState, reducers: { - setSubs + setSubs, }, extraReducers: (reducer) => { - reducer.addCase(updateGroupSubs.fulfilled, setSubs) - } -}) + reducer.addCase(updateGroupSubs.fulfilled, setSubs); + }, +}); export const groupConfigSelector = (state: RootState) => state.groupConfig; export default groupConfigSlice.reducer; diff --git a/admin-frontend/src/store/hooks.ts b/admin-frontend/src/store/hooks.ts index 99221df..5e6629e 100644 --- a/admin-frontend/src/store/hooks.ts +++ b/admin-frontend/src/store/hooks.ts @@ -1,5 +1,5 @@ -import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux"; -import {AppDispatch, RootState} from "."; +import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"; +import { AppDispatch, RootState } from "."; -export const useAppDispacher = () => useDispatch() -export const useAppSelector: TypedUseSelectorHook = useSelector +export const useAppDispacher = () => useDispatch(); +export const useAppSelector: TypedUseSelectorHook = useSelector; diff --git a/admin-frontend/src/store/index.ts b/admin-frontend/src/store/index.ts index 36f6821..18530e7 100644 --- a/admin-frontend/src/store/index.ts +++ b/admin-frontend/src/store/index.ts @@ -1,15 +1,15 @@ -import {configureStore} from "@reduxjs/toolkit"; +import { configureStore } from "@reduxjs/toolkit"; import loginSlice from "./loginSlice"; import globalConfSlice from "./globalConfSlice"; -import groupConfigSlice from './groupConfigSlice'; +import groupConfigSlice from "./groupConfigSlice"; const store = configureStore({ reducer: { login: loginSlice, globalConf: globalConfSlice, groupConfig: groupConfigSlice, - } -}) + }, +}); export default store; diff --git a/admin-frontend/src/store/loginSlice.ts b/admin-frontend/src/store/loginSlice.ts index 6db2da1..1b6ab40 100644 --- a/admin-frontend/src/store/loginSlice.ts +++ b/admin-frontend/src/store/loginSlice.ts @@ -1,108 +1,121 @@ -import { AnyAction, CaseReducer, createAsyncThunk, createSlice, PayloadAction, ThunkAction } from "@reduxjs/toolkit"; -import jwt_decode from 'jwt-decode'; +import { + AnyAction, + CaseReducer, + createAsyncThunk, + createSlice, + PayloadAction, + ThunkAction, +} from "@reduxjs/toolkit"; +import jwt_decode from "jwt-decode"; import { LoginStatus, TokenResp } from "src/utils/type"; import { auth } from "src/api/config"; -import {RootState} from "."; +import { RootState } from "."; const initialState: LoginStatus = { login: false, - type: '', - name: '', - id: '123', + type: "", + name: "", + id: "123", // groups: [], - token: '', - failed: false -} + token: "", + failed: false, +}; interface storedInfo { - type: string - name: string - id: string + type: string; + name: string; + id: string; } -const loginAction: CaseReducer> = (_, action) => { +const loginAction: CaseReducer> = ( + _, + action +) => { return { login: true, failed: false, type: action.payload.type, name: action.payload.name, id: action.payload.id, - token: action.payload.token - } -} + token: action.payload.token, + }; +}; export const login = createAsyncThunk( "auth/login", async (code: string) => { let res = await auth(code); if (res.status !== 200) { - throw Error("Login Error") + throw Error("Login Error"); } else { - localStorage.setItem('loginInfo', JSON.stringify({ - 'type': res.type, - 'name': res.name, - id: res.id, - })) - localStorage.setItem('token', res.token) + localStorage.setItem( + "loginInfo", + JSON.stringify({ + type: res.type, + name: res.name, + id: res.id, + }) + ); + localStorage.setItem("token", res.token); } - return res + return res; }, { condition: (_: string, { getState }) => { - const { login } = getState() as { login: LoginStatus } + const { login } = getState() as { login: LoginStatus }; return !login.login; - } + }, } -) - +); export const loginSlice = createSlice({ - name: 'auth', + name: "auth", initialState, reducers: { doLogin: loginAction, doClearLogin: (state) => { - state.login = false - } + state.login = false; + }, }, extraReducers: (builder) => { builder.addCase(login.fulfilled, loginAction); builder.addCase(login.rejected, (stat) => { - stat.failed = true - }) - } -}) + stat.failed = true; + }); + }, +}); -export const { doLogin, doClearLogin } = loginSlice.actions +export const { doLogin, doClearLogin } = loginSlice.actions; -export const loadLoginState = (): ThunkAction => +export const loadLoginState = + (): ThunkAction => (dispatch, getState) => { if (getState().login.login) { - return + return; } - const infoJson = localStorage.getItem('loginInfo') - const jwtToken = localStorage.getItem('token'); + const infoJson = localStorage.getItem("loginInfo"); + const jwtToken = localStorage.getItem("token"); if (infoJson && jwtToken) { const decodedJwt = jwt_decode(jwtToken) as { exp: number }; if (decodedJwt.exp < Date.now() / 1000) { - return + return; } - const info = JSON.parse(infoJson) as storedInfo + const info = JSON.parse(infoJson) as storedInfo; const payload: TokenResp = { ...info, status: 200, token: jwtToken, - } - dispatch(doLogin(payload)) + }; + dispatch(doLogin(payload)); } - } + }; -export const clearLoginStatus = (): ThunkAction => - (dispatch) => { - localStorage.removeItem('loginInfo') - localStorage.removeItem('token') - dispatch(doClearLogin()) - } -export const loginSelector = (state: RootState) => state.login +export const clearLoginStatus = + (): ThunkAction => (dispatch) => { + localStorage.removeItem("loginInfo"); + localStorage.removeItem("token"); + dispatch(doClearLogin()); + }; +export const loginSelector = (state: RootState) => state.login; -export default loginSlice.reducer +export default loginSlice.reducer; diff --git a/admin-frontend/src/utils/type.ts b/admin-frontend/src/utils/type.ts index e514ed0..6f99657 100644 --- a/admin-frontend/src/utils/type.ts +++ b/admin-frontend/src/utils/type.ts @@ -1,34 +1,34 @@ interface QQGroup { - id: string, - name: string, + id: string; + name: string; } export interface LoginStatus { - login: boolean - type: string - name: string - id: string + login: boolean; + type: string; + name: string; + id: string; // groups: Array - token: string, - failed: boolean, + token: string; + failed: boolean; } export type LoginContextType = { - login: LoginStatus - save: (status: LoginStatus) => void -} + login: LoginStatus; + save: (status: LoginStatus) => void; +}; export interface SubscribeConfig { - platformName: string - target: string - targetName: string - cats: Array - tags: Array + platformName: string; + target: string; + targetName: string; + cats: Array; + tags: Array; } export interface GlobalConf { - platformConf: AllPlatformConf, - loaded: boolean + platformConf: AllPlatformConf; + loaded: boolean; } export interface AllPlatformConf { @@ -36,34 +36,34 @@ export interface AllPlatformConf { } export interface CategoryConfig { - [idx: number]: string + [idx: number]: string; } export interface PlatformConfig { - name: string - categories: CategoryConfig - enabledTag: boolean, - platformName: string, - hasTarget: boolean + name: string; + categories: CategoryConfig; + enabledTag: boolean; + platformName: string; + hasTarget: boolean; } export interface TokenResp { - status: number, - token: string, - type: string, - id: string - name: string + status: number; + token: string; + type: string; + id: string; + name: string; } export interface SubscribeGroupDetail { - name: string, - subscribes: Array + name: string; + subscribes: Array; } export interface SubscribeResp { - [idx: string]: SubscribeGroupDetail + [idx: string]: SubscribeGroupDetail; } export interface TargetNameResp { - targetName: string + targetName: string; } From 7dbbaea6c8d698b767d8dbf4121bcfb0aeadf07d Mon Sep 17 00:00:00 2001 From: felinae98 <731499577@qq.com> Date: Sat, 12 Feb 2022 10:40:27 +0800 Subject: [PATCH 10/10] update config --- .pre-commit-config.yaml | 1 - pyproject.toml | 13 +++++++++++++ src/plugins/nonebot_bison/admin_page/__init__.py | 4 ++-- src/plugins/nonebot_bison/config.py | 2 +- src/plugins/nonebot_bison/config_manager.py | 2 +- src/plugins/nonebot_bison/platform/arknights.py | 2 +- src/plugins/nonebot_bison/platform/platform.py | 2 +- src/plugins/nonebot_bison/platform/rss.py | 2 +- src/plugins/nonebot_bison/platform/wechat.py | 4 ++-- src/plugins/nonebot_bison/platform/weibo.py | 4 ++-- src/plugins/nonebot_bison/post.py | 2 +- src/plugins/nonebot_bison/scheduler.py | 2 +- src/plugins/nonebot_bison/utils.py | 6 +++--- tests/platforms/test_arknights.py | 2 +- tests/platforms/test_bilibili.py | 2 +- tests/platforms/test_ncm_artist.py | 2 +- tests/platforms/test_ncm_radio.py | 2 +- tests/platforms/test_weibo.py | 6 +++--- 18 files changed, 36 insertions(+), 24 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index baf2d35..6796996 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,7 +9,6 @@ repos: rev: 5.10.1 hooks: - id: isort - args: ["--profile", "black", "--filter-files"] - repo: https://github.com/psf/black rev: 22.1.0 diff --git a/pyproject.toml b/pyproject.toml index 9ae2ecc..f0e540d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -55,3 +55,16 @@ markers = [ "compare: compare fetching result with rsshub", "render: render img by chrome" ] + +[tool.black] +line-length = 88 +target-version = ["py39", "py310"] +include = '\.pyi?$' +extend-exclude = ''' +''' + +[tool.isort] +profile = "black" +line_length = 88 +skip_gitignore = true +force_sort_within_sections = true diff --git a/src/plugins/nonebot_bison/admin_page/__init__.py b/src/plugins/nonebot_bison/admin_page/__init__.py index 0aba931..0d1733b 100644 --- a/src/plugins/nonebot_bison/admin_page/__init__.py +++ b/src/plugins/nonebot_bison/admin_page/__init__.py @@ -1,9 +1,8 @@ -import os from dataclasses import dataclass +import os from pathlib import Path from typing import Union -import socketio from fastapi.staticfiles import StaticFiles from nonebot import get_driver, on_command from nonebot.adapters.cqhttp.bot import Bot @@ -12,6 +11,7 @@ from nonebot.drivers.fastapi import Driver from nonebot.log import logger from nonebot.rule import to_me from nonebot.typing import T_State +import socketio from ..plugin_config import plugin_config from .api import ( diff --git a/src/plugins/nonebot_bison/config.py b/src/plugins/nonebot_bison/config.py index a021aeb..3a98006 100644 --- a/src/plugins/nonebot_bison/config.py +++ b/src/plugins/nonebot_bison/config.py @@ -1,5 +1,5 @@ -import os from collections import defaultdict +import os from os import path from typing import DefaultDict, Literal, Mapping, TypedDict diff --git a/src/plugins/nonebot_bison/config_manager.py b/src/plugins/nonebot_bison/config_manager.py index 8e96ea6..60569e4 100644 --- a/src/plugins/nonebot_bison/config_manager.py +++ b/src/plugins/nonebot_bison/config_manager.py @@ -204,7 +204,7 @@ def do_del_sub(del_sub: Type[Matcher]): config.del_subscribe( state.get("_user_id") or event.group_id, "group", - **state["sub_table"][index] + **state["sub_table"][index], ) except Exception as e: await del_sub.reject("删除错误") diff --git a/src/plugins/nonebot_bison/platform/arknights.py b/src/plugins/nonebot_bison/platform/arknights.py index f7b0002..a4b80f5 100644 --- a/src/plugins/nonebot_bison/platform/arknights.py +++ b/src/plugins/nonebot_bison/platform/arknights.py @@ -1,8 +1,8 @@ import json from typing import Any -import httpx from bs4 import BeautifulSoup as bs +import httpx from ..post import Post from ..types import Category, RawPost, Target diff --git a/src/plugins/nonebot_bison/platform/platform.py b/src/plugins/nonebot_bison/platform/platform.py index 1bec3d8..34cdb76 100644 --- a/src/plugins/nonebot_bison/platform/platform.py +++ b/src/plugins/nonebot_bison/platform/platform.py @@ -1,7 +1,7 @@ -import time from abc import ABC, abstractmethod from collections import defaultdict from dataclasses import dataclass +import time from typing import Any, Collection, Literal, Optional import httpx diff --git a/src/plugins/nonebot_bison/platform/rss.py b/src/plugins/nonebot_bison/platform/rss.py index 330d93d..2adde67 100644 --- a/src/plugins/nonebot_bison/platform/rss.py +++ b/src/plugins/nonebot_bison/platform/rss.py @@ -1,9 +1,9 @@ import calendar from typing import Any, Optional +from bs4 import BeautifulSoup as bs import feedparser import httpx -from bs4 import BeautifulSoup as bs from ..post import Post from ..types import RawPost, Target diff --git a/src/plugins/nonebot_bison/platform/wechat.py b/src/plugins/nonebot_bison/platform/wechat.py index d5f5487..b45839a 100644 --- a/src/plugins/nonebot_bison/platform/wechat.py +++ b/src/plugins/nonebot_bison/platform/wechat.py @@ -1,11 +1,11 @@ +from datetime import datetime import hashlib import json import re -from datetime import datetime from typing import Any, Optional -import httpx from bs4 import BeautifulSoup as bs +import httpx from ..post import Post from ..types import * diff --git a/src/plugins/nonebot_bison/platform/weibo.py b/src/plugins/nonebot_bison/platform/weibo.py index 365e3b2..a69b3b2 100644 --- a/src/plugins/nonebot_bison/platform/weibo.py +++ b/src/plugins/nonebot_bison/platform/weibo.py @@ -1,10 +1,10 @@ +from datetime import datetime import json import re -from datetime import datetime from typing import Any, Optional -import httpx from bs4 import BeautifulSoup as bs +import httpx from nonebot import logger from ..post import Post diff --git a/src/plugins/nonebot_bison/post.py b/src/plugins/nonebot_bison/post.py index 604295d..7122566 100644 --- a/src/plugins/nonebot_bison/post.py +++ b/src/plugins/nonebot_bison/post.py @@ -4,10 +4,10 @@ from functools import reduce from io import BytesIO from typing import Optional, Union +from PIL import Image import httpx from nonebot import logger from nonebot.adapters.cqhttp.message import Message, MessageSegment -from PIL import Image from .plugin_config import plugin_config from .utils import parse_text diff --git a/src/plugins/nonebot_bison/scheduler.py b/src/plugins/nonebot_bison/scheduler.py index 9e8eaf7..ba9cc2c 100644 --- a/src/plugins/nonebot_bison/scheduler.py +++ b/src/plugins/nonebot_bison/scheduler.py @@ -1,8 +1,8 @@ import asyncio import logging -import nonebot from apscheduler.schedulers.asyncio import AsyncIOScheduler +import nonebot from nonebot import get_driver, logger from nonebot.log import LoguruHandler diff --git a/src/plugins/nonebot_bison/utils.py b/src/plugins/nonebot_bison/utils.py index 63774d7..5730a8b 100644 --- a/src/plugins/nonebot_bison/utils.py +++ b/src/plugins/nonebot_bison/utils.py @@ -1,17 +1,17 @@ import asyncio import base64 +from html import escape import os +from pathlib import Path import platform import re import subprocess import sys -from html import escape -from pathlib import Path from time import asctime from typing import Awaitable, Callable, Optional, Union -import nonebot from bs4 import BeautifulSoup as bs +import nonebot from nonebot.adapters.cqhttp.message import MessageSegment from nonebot.log import default_format, logger from playwright._impl._driver import compute_driver_executable diff --git a/tests/platforms/test_arknights.py b/tests/platforms/test_arknights.py index 7734791..a645c03 100644 --- a/tests/platforms/test_arknights.py +++ b/tests/platforms/test_arknights.py @@ -1,8 +1,8 @@ import typing +from httpx import Response import pytest import respx -from httpx import Response if typing.TYPE_CHECKING: import sys diff --git a/tests/platforms/test_bilibili.py b/tests/platforms/test_bilibili.py index 4b99c83..3f6b5a3 100644 --- a/tests/platforms/test_bilibili.py +++ b/tests/platforms/test_bilibili.py @@ -1,7 +1,7 @@ import typing -import pytest from httpx import Response +import pytest if typing.TYPE_CHECKING: import sys diff --git a/tests/platforms/test_ncm_artist.py b/tests/platforms/test_ncm_artist.py index 37242e5..e11bf84 100644 --- a/tests/platforms/test_ncm_artist.py +++ b/tests/platforms/test_ncm_artist.py @@ -1,9 +1,9 @@ import time import typing +from httpx import Response import pytest import respx -from httpx import Response from .utils import get_json diff --git a/tests/platforms/test_ncm_radio.py b/tests/platforms/test_ncm_radio.py index 3ce6cfe..670a458 100644 --- a/tests/platforms/test_ncm_radio.py +++ b/tests/platforms/test_ncm_radio.py @@ -1,9 +1,9 @@ import time import typing +from httpx import Response import pytest import respx -from httpx import Response from .utils import get_json diff --git a/tests/platforms/test_weibo.py b/tests/platforms/test_weibo.py index 68ee873..11b7d43 100644 --- a/tests/platforms/test_weibo.py +++ b/tests/platforms/test_weibo.py @@ -1,11 +1,11 @@ -import typing from datetime import datetime +import typing import feedparser -import pytest -import respx from httpx import Response +import pytest from pytz import timezone +import respx if typing.TYPE_CHECKING: import sys