19 Commits

Author SHA1 Message Date
suyiiyii 012df0593d 🔀 merge main 2024-09-03 09:49:19 +08:00
suyiiyii 7901b845ea 初步实现携带cookie请求 2024-09-02 23:13:29 +08:00
github-actions[bot] ccbed746da 📝 Update changelog 2024-09-01 14:34:29 +00:00
dependabot[bot] c523b3a811 ⬆️ Bump webpack from 5.88.2 to 5.94.0 in /admin-frontend (#619)
Bumps [webpack](https://github.com/webpack/webpack) from 5.88.2 to 5.94.0.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.88.2...v5.94.0)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-01 22:33:03 +08:00
suyiiyii d6205867b8 无权限用户尝试添加订阅时返回提示信息 (#617)
*  无权限用户尝试添加订阅时返回提示信息

*  添加no_permission_matcher相关的单元测试

* 🐛 优化无权限提示的排版

* 🐛 移除没有必要的命令
2024-09-01 22:32:47 +08:00
github-actions[bot] 83cd0a741e 📝 Update changelog 2024-09-01 14:30:29 +00:00
Azide b8b49a5ce5 🐛 B站请求策略阶段行为优化 (#610)
* 🐛 调整ruff的pytest警告

* 🐛 调整导入关系警告

* 🐛 删除奇怪无用的赋值和取值逻辑

*  不同测试部分所用变量应加以区分

* 🐛 subs_io model添加默认值

* 🐛 修完所有的 ruff PT001 警告

* 🔧 按ruff建议修改ruff配置

warning: The top-level linter settings are deprecated in favour of their counterparts in the `lint` section. Please update the following options in `pyproject.toml`:
  - 'ignore' -> 'lint.ignore'
  - 'select' -> 'lint.select'

* 🔊 降低B站请求策略非Raise阶段API352的日志等级

* 🐛 Raise阶段应该 raise err

* 🔊 日志添加平台名

* 🐛 bzhanhan调度继续降低至60s

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-09-01 22:30:02 +08:00
github-actions[bot] afcc536334 📝 Update changelog 2024-09-01 14:24:42 +00:00
suyiiyii cf38500be7 🐛 Rss 不再删除格式化字符 2024-09-01 22:23:02 +08:00
suyiiyii 1cd778c2e0 ♻️ cookie 组件不再与 user 关联 2024-08-31 23:07:43 +08:00
suyiiyii c828fd94e4 ♻️ cookie 组件不再与 user 关联 2024-08-31 23:04:49 +08:00
github-actions[bot] 7d80b44d2a 📝 Update changelog 2024-08-30 02:26:50 +00:00
uy_sun 60e6f05cf4 fix tests 2024-08-30 10:26:22 +08:00
felinae98 8a21ca2a1c 🐛 forbid adding platform that needs browser in no-browser env 2024-08-30 10:26:22 +08:00
suyiiyii ffae6f2ec5 添加cookie的时候显示关联的cookie 2024-08-26 18:09:26 +08:00
suyiiyii 6f20dbf358 支持对话关联cookie到订阅目标 2024-08-26 17:32:36 +08:00
suyiiyii b655eff755 支持对话添加cookie 2024-08-26 10:37:03 +08:00
suyiiyii c264ad374b 🐛 stash 2024-08-22 21:50:25 +08:00
suyiiyii 7913f7485a 添加cookie相关的数据库表 2024-08-22 20:55:39 +08:00
25 changed files with 8157 additions and 5557 deletions
+4
View File
@@ -4,6 +4,10 @@
### Bug 修复 ### Bug 修复
- 无权限用户尝试添加订阅时返回提示信息 [@suyiiyii](https://github.com/suyiiyii) ([#617](https://github.com/MountainDash/nonebot-bison/pull/617))
- B站请求策略阶段行为优化 [@AzideCupric](https://github.com/AzideCupric) ([#610](https://github.com/MountainDash/nonebot-bison/pull/610))
- Rss 不再删除格式化字符 [@suyiiyii](https://github.com/suyiiyii) ([#615](https://github.com/MountainDash/nonebot-bison/pull/615))
- forbid adding platform that needs browser in no-browser env [@felinae98](https://github.com/felinae98) ([#609](https://github.com/MountainDash/nonebot-bison/pull/609))
- 修正项目的代码警告 [@AzideCupric](https://github.com/AzideCupric) ([#614](https://github.com/MountainDash/nonebot-bison/pull/614)) - 修正项目的代码警告 [@AzideCupric](https://github.com/AzideCupric) ([#614](https://github.com/MountainDash/nonebot-bison/pull/614))
- 修复 anonymous_site() 无法正确工作的问题 [@felinae98](https://github.com/felinae98) ([#606](https://github.com/MountainDash/nonebot-bison/pull/606)) - 修复 anonymous_site() 无法正确工作的问题 [@felinae98](https://github.com/felinae98) ([#606](https://github.com/MountainDash/nonebot-bison/pull/606))
+7465 -5537
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -1,4 +1,5 @@
from .types import Target from .types import Target
from .config.db_model import Cookie
from .scheduler import scheduler_dict from .scheduler import scheduler_dict
from .platform import platform_manager from .platform import platform_manager
@@ -10,3 +11,13 @@ async def check_sub_target(platform_name: str, target: Target):
client = await scheduler.client_mgr.get_query_name_client() client = await scheduler.client_mgr.get_query_name_client()
return await platform_manager[platform_name].get_target_name(client, target) return await platform_manager[platform_name].get_target_name(client, target)
async def check_sub_target_cookie(platform_name: str, target: Target, cookie: str):
# TODO
return "check pass"
async def get_cookie_friendly_name(cookie: Cookie):
# TODO
return f"{cookie.platform_name} [{cookie.content[:10]}]"
+76 -2
View File
@@ -12,8 +12,8 @@ from nonebot_plugin_datastore import create_session
from ..types import Tag from ..types import Tag
from ..types import Target as T_Target from ..types import Target as T_Target
from .utils import NoSuchTargetException from .utils import NoSuchTargetException, DuplicateCookieTargetException
from .db_model import User, Target, Subscribe, ScheduleTimeWeight from .db_model import User, Cookie, Target, Subscribe, CookieTarget, ScheduleTimeWeight
from ..types import Category, UserSubInfo, WeightConfig, TimeWeightConfig, PlatformWeightConfigResp from ..types import Category, UserSubInfo, WeightConfig, TimeWeightConfig, PlatformWeightConfigResp
@@ -259,5 +259,79 @@ class DBConfig:
) )
return res return res
async def get_cookie(self, platform_name: str = None, target: T_Target = None) -> list[Cookie]:
async with create_session() as sess:
query = select(Cookie).distinct()
if platform_name:
query = query.where(Cookie.platform_name == platform_name)
query = query.outerjoin(CookieTarget).options(selectinload(Cookie.targets))
res = (await sess.scalars(query)).all()
if target:
query = select(CookieTarget.cookie_id).join(Target).where(Target.target == target)
ids = set((await sess.scalars(query)).all())
res = [cookie for cookie in res if cookie.id in ids]
return res
async def add_cookie(self, platform_name: str, content: str) -> int:
async with create_session() as sess:
cookie = Cookie(platform_name=platform_name, content=content)
sess.add(cookie)
await sess.commit()
await sess.refresh(cookie)
return cookie.id
async def update_cookie(self, cookie: Cookie):
async with create_session() as sess:
cookie_in_db: Cookie | None = await sess.scalar(select(Cookie).where(Cookie.id == cookie.id))
if not cookie_in_db:
return
cookie_in_db.content = cookie.content
cookie_in_db.last_usage = cookie.last_usage
cookie_in_db.status = cookie.status
cookie_in_db.tags = cookie.tags
await sess.commit()
async def delete_cookie(self, cookie_id: int):
async with create_session() as sess:
await sess.execute(delete(Cookie).where(Cookie.id == cookie_id))
await sess.commit()
async def get_cookie_by_target(self, target: T_Target, platform_name: str) -> list[Cookie]:
async with create_session() as sess:
query = (
select(Cookie)
.join(CookieTarget)
.join(Target)
.where(Target.platform_name == platform_name, Target.target == target)
)
return list((await sess.scalars(query)).all())
async def add_cookie_target(self, target: T_Target, platform_name: str, cookie_id: int):
async with create_session() as sess:
target_obj = await sess.scalar(
select(Target).where(Target.platform_name == platform_name, Target.target == target)
)
# check if relation exists
cookie_target = await sess.scalar(
select(CookieTarget).where(CookieTarget.target == target_obj, CookieTarget.cookie_id == cookie_id)
)
if cookie_target:
raise DuplicateCookieTargetException()
cookie_obj = await sess.scalar(select(Cookie).where(Cookie.id == cookie_id))
cookie_target = CookieTarget(target=target_obj, cookie=cookie_obj)
sess.add(cookie_target)
await sess.commit()
async def delete_cookie_target(self, target: T_Target, platform_name: str, cookie_id: int):
async with create_session() as sess:
target_obj = await sess.scalar(
select(Target).where(Target.platform_name == platform_name, Target.target == target)
)
cookie_obj = await sess.scalar(select(Cookie).where(Cookie.id == cookie_id))
await sess.execute(
delete(CookieTarget).where(CookieTarget.target == target_obj, CookieTarget.cookie == cookie_obj)
)
await sess.commit()
config = DBConfig() config = DBConfig()
+23 -1
View File
@@ -1,4 +1,5 @@
import datetime import datetime
from typing import Any
from pathlib import Path from pathlib import Path
from nonebot_plugin_saa import PlatformTarget from nonebot_plugin_saa import PlatformTarget
@@ -6,7 +7,7 @@ from sqlalchemy.dialects.postgresql import JSONB
from nonebot.compat import PYDANTIC_V2, ConfigDict from nonebot.compat import PYDANTIC_V2, ConfigDict
from nonebot_plugin_datastore import get_plugin_data from nonebot_plugin_datastore import get_plugin_data
from sqlalchemy.orm import Mapped, relationship, mapped_column from sqlalchemy.orm import Mapped, relationship, mapped_column
from sqlalchemy import JSON, String, ForeignKey, UniqueConstraint from sqlalchemy import JSON, String, DateTime, ForeignKey, UniqueConstraint
from ..types import Tag, Category from ..types import Tag, Category
@@ -36,6 +37,7 @@ class Target(Model):
subscribes: Mapped[list["Subscribe"]] = relationship(back_populates="target") subscribes: Mapped[list["Subscribe"]] = relationship(back_populates="target")
time_weight: Mapped[list["ScheduleTimeWeight"]] = relationship(back_populates="target") time_weight: Mapped[list["ScheduleTimeWeight"]] = relationship(back_populates="target")
cookies: Mapped[list["CookieTarget"]] = relationship(back_populates="target")
class ScheduleTimeWeight(Model): class ScheduleTimeWeight(Model):
@@ -66,3 +68,23 @@ class Subscribe(Model):
target: Mapped[Target] = relationship(back_populates="subscribes") target: Mapped[Target] = relationship(back_populates="subscribes")
user: Mapped[User] = relationship(back_populates="subscribes") user: Mapped[User] = relationship(back_populates="subscribes")
class Cookie(Model):
id: Mapped[int] = mapped_column(primary_key=True)
platform_name: Mapped[str] = mapped_column(String(20))
content: Mapped[str] = mapped_column(String(1024))
last_usage: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime(1970, 1, 1))
status: Mapped[str] = mapped_column(String(20), default="")
tags: Mapped[dict[str, Any]] = mapped_column(JSON().with_variant(JSONB, "postgresql"), default={})
targets: Mapped[list["CookieTarget"]] = relationship(back_populates="cookie")
class CookieTarget(Model):
id: Mapped[int] = mapped_column(primary_key=True)
target_id: Mapped[int] = mapped_column(ForeignKey("nonebot_bison_target.id", ondelete="CASCADE"))
cookie_id: Mapped[int] = mapped_column(ForeignKey("nonebot_bison_cookie.id", ondelete="CASCADE"))
target: Mapped[Target] = relationship(back_populates="cookies")
cookie: Mapped[Cookie] = relationship(back_populates="targets")
@@ -0,0 +1,59 @@
"""empty message
Revision ID: 590dc2911ea7
Revises: f9baef347cc8
Create Date: 2024-08-31 23:06:02.123932
"""
import sqlalchemy as sa
from alembic import op
from sqlalchemy import Text
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision = "590dc2911ea7"
down_revision = "f9baef347cc8"
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"nonebot_bison_cookie",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("platform_name", sa.String(length=20), nullable=False),
sa.Column("content", sa.String(length=1024), nullable=False),
sa.Column("last_usage", sa.DateTime(), nullable=False),
sa.Column("status", sa.String(length=20), nullable=False),
sa.Column("tags", sa.JSON().with_variant(postgresql.JSONB(astext_type=Text()), "postgresql"), nullable=False),
sa.PrimaryKeyConstraint("id", name=op.f("pk_nonebot_bison_cookie")),
)
op.create_table(
"nonebot_bison_cookietarget",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("target_id", sa.Integer(), nullable=False),
sa.Column("cookie_id", sa.Integer(), nullable=False),
sa.ForeignKeyConstraint(
["cookie_id"],
["nonebot_bison_cookie.id"],
name=op.f("fk_nonebot_bison_cookietarget_cookie_id_nonebot_bison_cookie"),
ondelete="CASCADE",
),
sa.ForeignKeyConstraint(
["target_id"],
["nonebot_bison_target.id"],
name=op.f("fk_nonebot_bison_cookietarget_target_id_nonebot_bison_target"),
ondelete="CASCADE",
),
sa.PrimaryKeyConstraint("id", name=op.f("pk_nonebot_bison_cookietarget")),
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table("nonebot_bison_cookietarget")
op.drop_table("nonebot_bison_cookie")
# ### end Alembic commands ###
+4
View File
@@ -8,3 +8,7 @@ class NoSuchSubscribeException(Exception):
class NoSuchTargetException(Exception): class NoSuchTargetException(Exception):
pass pass
class DuplicateCookieTargetException(Exception):
pass
+13
View File
@@ -3,6 +3,7 @@ from pkgutil import iter_modules
from collections import defaultdict from collections import defaultdict
from importlib import import_module from importlib import import_module
from ..plugin_config import plugin_config
from .platform import Platform, make_no_target_group from .platform import Platform, make_no_target_group
_package_dir = str(Path(__file__).resolve().parent) _package_dir = str(Path(__file__).resolve().parent)
@@ -22,3 +23,15 @@ for name, platform_list in _platform_list.items():
platform_manager[name] = platform_list[0] platform_manager[name] = platform_list[0]
else: else:
platform_manager[name] = make_no_target_group(platform_list) platform_manager[name] = make_no_target_group(platform_list)
def _get_unavailable_platforms() -> dict[str, str]:
res = {}
for name, platform in platform_manager.items():
if platform.site.require_browser and not plugin_config.bison_use_browser:
res[name] = "需要启用 bison_use_browser"
return res
# platform => reason for not available
unavailable_paltforms: dict[str, str] = _get_unavailable_platforms()
+7 -3
View File
@@ -236,15 +236,19 @@ def retry_for_352(api_func: Callable[[TBilibili, Target], Awaitable[list[DynRawP
case RetryState.NROMAL | RetryState.REFRESH | RetryState.RAISE: case RetryState.NROMAL | RetryState.REFRESH | RetryState.RAISE:
try: try:
res = await api_func(bls, *args, **kwargs) res = await api_func(bls, *args, **kwargs)
except ApiCode352Error: except ApiCode352Error as e:
logger.error("API 352 错误") logger.warning("本次 Bilibili API 请求返回 352 错误")
await _retry_fsm.emit(RetryEvent.REQUEST_AND_RAISE) await _retry_fsm.emit(RetryEvent.REQUEST_AND_RAISE)
if _retry_fsm.current_state == RetryState.RAISE:
raise e
return [] return []
else: else:
await _retry_fsm.emit(RetryEvent.REQUEST_AND_SUCCESS) await _retry_fsm.emit(RetryEvent.REQUEST_AND_SUCCESS)
return res return res
case RetryState.BACKOFF: case RetryState.BACKOFF:
logger.warning("回避中,不请求") logger.warning("本次 Bilibili 请求回避中,不请求")
await _retry_fsm.emit(RetryEvent.IN_BACKOFF_TIME) await _retry_fsm.emit(RetryEvent.IN_BACKOFF_TIME)
return [] return []
case _: case _:
+1 -1
View File
@@ -68,7 +68,7 @@ class BilibiliClientManager(ClientManager):
class BilibiliSite(Site): class BilibiliSite(Site):
name = "bilibili.com" name = "bilibili.com"
schedule_setting = {"seconds": 50} schedule_setting = {"seconds": 60}
schedule_type = "interval" schedule_type = "interval"
client_mgr = BilibiliClientManager client_mgr = BilibiliClientManager
require_browser = True require_browser = True
+6 -4
View File
@@ -9,13 +9,15 @@ from bs4 import BeautifulSoup as bs
from ..post import Post from ..post import Post
from .platform import NewMessage from .platform import NewMessage
from ..types import Target, RawPost from ..types import Target, RawPost
from ..utils import Site, text_fletten, text_similarity from ..utils import Site, text_similarity
from ..utils.site import create_cookie_client_manager
class RssSite(Site): class RssSite(Site):
name = "rss" name = "rss"
schedule_type = "interval" schedule_type = "interval"
schedule_setting = {"seconds": 30} schedule_setting = {"seconds": 30}
client_mgr = create_cookie_client_manager("rss")
class RssPost(Post): class RssPost(Post):
@@ -32,7 +34,7 @@ class RssPost(Post):
for p in soup.find_all("p"): for p in soup.find_all("p"):
p.insert_after("\n") p.insert_after("\n")
return text_fletten(soup.get_text()) return soup.get_text()
class Rss(NewMessage): class Rss(NewMessage):
@@ -63,7 +65,7 @@ class Rss(NewMessage):
return post.id return post.id
async def get_sub_list(self, target: Target) -> list[RawPost]: async def get_sub_list(self, target: Target) -> list[RawPost]:
client = await self.ctx.get_client() client = await self.ctx.get_client(target)
res = await client.get(target, timeout=10.0) res = await client.get(target, timeout=10.0)
feed = feedparser.parse(res) feed = feedparser.parse(res)
entries = feed.entries entries = feed.entries
@@ -82,7 +84,7 @@ class Rss(NewMessage):
async def parse(self, raw_post: RawPost) -> Post: async def parse(self, raw_post: RawPost) -> Post:
title = raw_post.get("title", "") title = raw_post.get("title", "")
soup = bs(raw_post.description, "html.parser") soup = bs(raw_post.description, "html.parser")
desc = soup.text.strip() desc = raw_post.description
title, desc = self._text_process(title, desc) title, desc = self._text_process(title, desc)
pics = [x.attrs["src"] for x in soup("img")] pics = [x.attrs["src"] for x in soup("img")]
if raw_post.get("media_content"): if raw_post.get("media_content"):
+33 -2
View File
@@ -14,6 +14,8 @@ from nonebot.adapters.onebot.v11.event import PrivateMessageEvent
from .add_sub import do_add_sub from .add_sub import do_add_sub
from .del_sub import do_del_sub from .del_sub import do_del_sub
from .query_sub import do_query_sub from .query_sub import do_query_sub
from .add_cookie import do_add_cookie
from .add_cookie_target import do_add_cookie_target
from .utils import common_platform, admin_permission, gen_handle_cancel, configurable_to_me, set_target_user_info from .utils import common_platform, admin_permission, gen_handle_cancel, configurable_to_me, set_target_user_info
add_sub_matcher = on_command( add_sub_matcher = on_command(
@@ -26,12 +28,10 @@ add_sub_matcher = on_command(
add_sub_matcher.handle()(set_target_user_info) add_sub_matcher.handle()(set_target_user_info)
do_add_sub(add_sub_matcher) do_add_sub(add_sub_matcher)
query_sub_matcher = on_command("查询订阅", rule=configurable_to_me, priority=5, block=True) query_sub_matcher = on_command("查询订阅", rule=configurable_to_me, priority=5, block=True)
query_sub_matcher.handle()(set_target_user_info) query_sub_matcher.handle()(set_target_user_info)
do_query_sub(query_sub_matcher) do_query_sub(query_sub_matcher)
del_sub_matcher = on_command( del_sub_matcher = on_command(
"删除订阅", "删除订阅",
rule=configurable_to_me, rule=configurable_to_me,
@@ -42,6 +42,26 @@ del_sub_matcher = on_command(
del_sub_matcher.handle()(set_target_user_info) del_sub_matcher.handle()(set_target_user_info)
do_del_sub(del_sub_matcher) do_del_sub(del_sub_matcher)
add_cookie_matcher = on_command(
"添加cookie",
rule=configurable_to_me,
permission=admin_permission(),
priority=5,
block=True,
)
add_cookie_matcher.handle()(set_target_user_info)
do_add_cookie(add_cookie_matcher)
add_cookie_target_matcher = on_command(
"关联cookie",
rule=configurable_to_me,
permission=admin_permission(),
priority=5,
block=True,
)
add_cookie_target_matcher.handle()(set_target_user_info)
do_add_cookie_target(add_cookie_target_matcher)
group_manage_matcher = on_command("群管理", rule=to_me(), permission=SUPERUSER, priority=4, block=True) group_manage_matcher = on_command("群管理", rule=to_me(), permission=SUPERUSER, priority=4, block=True)
group_handle_cancel = gen_handle_cancel(group_manage_matcher, "已取消") group_handle_cancel = gen_handle_cancel(group_manage_matcher, "已取消")
@@ -108,10 +128,21 @@ async def do_dispatch_command(
asyncio.create_task(new_matcher_ins.run(bot, event, state)) asyncio.create_task(new_matcher_ins.run(bot, event, state))
no_permission_matcher = on_command(
"添加订阅", rule=configurable_to_me, aliases={"删除订阅", "群管理"}, priority=8, block=True
)
@no_permission_matcher.handle()
async def send_no_permission():
await no_permission_matcher.finish("您没有权限进行此操作,请联系 Bot 管理员")
__all__ = [ __all__ = [
"common_platform", "common_platform",
"add_sub_matcher", "add_sub_matcher",
"query_sub_matcher", "query_sub_matcher",
"del_sub_matcher", "del_sub_matcher",
"group_manage_matcher", "group_manage_matcher",
"no_permission_matcher",
] ]
+60
View File
@@ -0,0 +1,60 @@
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.params import Arg, ArgPlainText
from nonebot.adapters import Message, MessageTemplate
from ..types import Target
from ..config import config
from ..platform import platform_manager
from ..apis import check_sub_target_cookie
from .utils import common_platform, gen_handle_cancel
def do_add_cookie(add_cookie: type[Matcher]):
handle_cancel = gen_handle_cancel(add_cookie, "已中止添加cookie")
@add_cookie.handle()
async def init_promote(state: T_State):
state["_prompt"] = (
"请输入想要添加 Cookie 的平台,目前支持,请输入冒号左边的名称:\n"
+ "".join(
[f"{platform_name}: {platform_manager[platform_name].name}\n" for platform_name in common_platform]
)
+ "要查看全部平台请输入:“全部”\n中止添加cookie过程请输入:“取消”"
)
@add_cookie.got("platform", MessageTemplate("{_prompt}"), [handle_cancel])
async def parse_platform(state: T_State, platform: str = ArgPlainText()) -> None:
if platform == "全部":
message = "全部平台\n" + "\n".join(
[f"{platform_name}: {platform.name}" for platform_name, platform in platform_manager.items()]
)
await add_cookie.reject(message)
elif platform == "取消":
await add_cookie.finish("已中止添加cookie")
elif platform in platform_manager:
state["platform"] = platform
else:
await add_cookie.reject("平台输入错误")
@add_cookie.handle()
async def prepare_get_id(matcher: Matcher, state: T_State):
cur_platform = platform_manager[state["platform"]]
if cur_platform.has_target:
state["_prompt"] = "请输入 Cookie"
else:
matcher.set_arg("cookie", None) # type: ignore
state["id"] = "default"
@add_cookie.got("cookie", MessageTemplate("{_prompt}"), [handle_cancel])
async def got_cookie(state: T_State, cookie: Message = Arg()):
cookie_text = cookie.extract_plain_text()
state["cookie"] = cookie_text
state["name"] = await check_sub_target_cookie(state["platform"], Target(""), cookie_text)
@add_cookie.handle()
async def add_cookie_process(state: T_State):
await config.add_cookie(state["platform"], state["cookie"])
await add_cookie.finish(
f"已添加 Cookie: {state['cookie']} 到平台 {state['platform']}" + "\n请使用“关联cookie”为 Cookie 关联订阅"
)
@@ -0,0 +1,63 @@
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.params import ArgPlainText
from nonebot_plugin_saa import MessageFactory
from nonebot.internal.adapter import MessageTemplate
from ..config import config
from ..utils import parse_text
from ..apis import get_cookie_friendly_name
from .utils import gen_handle_cancel, generate_sub_list_text
def do_add_cookie_target(add_cookie_target_matcher: type[Matcher]):
handle_cancel = gen_handle_cancel(add_cookie_target_matcher, "已中止关联 cookie")
@add_cookie_target_matcher.handle()
async def init_promote(state: T_State):
res = await generate_sub_list_text(add_cookie_target_matcher, state, is_index=True, is_show_cookie=True)
res += "请输入要关联 cookie 的订阅的序号\n输入'取消'中止"
await MessageFactory(await parse_text(res)).send()
@add_cookie_target_matcher.got("target_idx", parameterless=[handle_cancel])
async def got_target_idx(state: T_State, target_idx: str = ArgPlainText()):
try:
target_idx = int(target_idx)
state["target"] = state["sub_table"][target_idx]
except Exception:
await add_cookie_target_matcher.reject("序号错误")
@add_cookie_target_matcher.handle()
async def init_promote_cookie(state: T_State):
cookies = await config.get_cookie(platform_name=state["target"]["platform_name"])
associated_cookies = await config.get_cookie(
target=state["target"]["target"],
platform_name=state["target"]["platform_name"],
)
associated_cookie_ids = {cookie.id for cookie in associated_cookies}
cookies = [cookie for cookie in cookies if cookie.id not in associated_cookie_ids]
if not cookies:
await add_cookie_target_matcher.finish(
"当前平台暂无可关联的 Cookie,请使用“添加cookie”命令添加或检查已关联的 Cookie"
)
state["cookies"] = cookies
state["_prompt"] = "请选择一个 Cookie,已关联的 Cookie 不会显示\n" + "\n".join(
[f"{idx}. {await get_cookie_friendly_name(cookie)}" for idx, cookie in enumerate(cookies, 1)]
)
@add_cookie_target_matcher.got("cookie_idx", MessageTemplate("{_prompt}"), [handle_cancel])
async def got_cookie_idx(state: T_State, cookie_idx: str = ArgPlainText()):
try:
cookie_idx = int(cookie_idx)
state["cookie"] = state["cookies"][cookie_idx - 1]
except Exception:
await add_cookie_target_matcher.reject("序号错误")
@add_cookie_target_matcher.handle()
async def add_cookie_target_process(state: T_State):
await config.add_cookie_target(state["target"]["target"], state["target"]["platform_name"], state["cookie"].id)
await add_cookie_target_matcher.finish(
f"已关联 Cookie: {await get_cookie_friendly_name(state['cookie'])} "
f"到订阅 {state['target']['platform_name']} {state['target']['target']}"
)
+3 -1
View File
@@ -9,9 +9,9 @@ from nonebot_plugin_saa import Text, PlatformTarget, SupportedAdapters
from ..types import Target from ..types import Target
from ..config import config from ..config import config
from ..apis import check_sub_target from ..apis import check_sub_target
from ..platform import Platform, platform_manager
from ..config.db_config import SubscribeDupException from ..config.db_config import SubscribeDupException
from .utils import common_platform, ensure_user_info, gen_handle_cancel from .utils import common_platform, ensure_user_info, gen_handle_cancel
from ..platform import Platform, platform_manager, unavailable_paltforms
def do_add_sub(add_sub: type[Matcher]): def do_add_sub(add_sub: type[Matcher]):
@@ -39,6 +39,8 @@ def do_add_sub(add_sub: type[Matcher]):
elif platform == "取消": elif platform == "取消":
await add_sub.finish("已中止订阅") await add_sub.finish("已中止订阅")
elif platform in platform_manager: elif platform in platform_manager:
if platform in unavailable_paltforms:
await add_sub.finish(f"无法订阅 {platform}{unavailable_paltforms[platform]}")
state["platform"] = platform state["platform"] = platform
else: else:
await add_sub.reject("平台输入错误") await add_sub.reject("平台输入错误")
+49 -1
View File
@@ -1,16 +1,21 @@
import contextlib import contextlib
from typing import Annotated from typing import Annotated
from itertools import groupby
from operator import attrgetter
from nonebot.rule import Rule from nonebot.rule import Rule
from nonebot.adapters import Event from nonebot.adapters import Event
from nonebot.typing import T_State from nonebot.typing import T_State
from nonebot.matcher import Matcher from nonebot.matcher import Matcher
from nonebot.permission import SUPERUSER from nonebot.permission import SUPERUSER
from nonebot_plugin_saa import extract_target
from nonebot.params import Depends, EventToMe, EventPlainText from nonebot.params import Depends, EventToMe, EventPlainText
from nonebot_plugin_saa import PlatformTarget, extract_target
from ..config import config
from ..types import Category
from ..platform import platform_manager from ..platform import platform_manager
from ..plugin_config import plugin_config from ..plugin_config import plugin_config
from ..apis import get_cookie_friendly_name
def _configurable_to_me(to_me: bool = EventToMe()): def _configurable_to_me(to_me: bool = EventToMe()):
@@ -60,3 +65,46 @@ def admin_permission():
permission = permission | GROUP_ADMIN | GROUP_OWNER permission = permission | GROUP_ADMIN | GROUP_OWNER
return permission return permission
async def generate_sub_list_text(
matcher: type[Matcher], state: T_State, user_info: PlatformTarget = None, is_index=False, is_show_cookie=False
):
if user_info:
sub_list = await config.list_subscribe(user_info)
else:
sub_list = await config.list_subs_with_all_info()
sub_list = [
next(group)
for key, group in groupby(sorted(sub_list, key=attrgetter("target_id")), key=attrgetter("target_id"))
]
if not sub_list:
await matcher.finish("暂无已订阅账号\n请使用“添加订阅”命令添加订阅")
res = "订阅的帐号为:\n"
state["sub_table"] = {}
for index, sub in enumerate(sub_list, 1):
state["sub_table"][index] = {
"platform_name": sub.target.platform_name,
"target": sub.target.target,
}
res += f"{index} " if is_index else ""
res += f"{sub.target.platform_name} {sub.target.target_name} {sub.target.target}\n"
if platform := platform_manager.get(sub.target.platform_name):
if platform.categories:
res += " [{}]".format(", ".join(platform.categories[Category(x)] for x in sub.categories)) + "\n"
if platform.enable_tag:
if sub.tags:
res += " {}".format(", ".join(sub.tags)) + "\n"
if is_show_cookie:
target_cookies = await config.get_cookie(
target=sub.target.target, platform_name=sub.target.platform_name
)
if target_cookies:
res += " 关联的 Cookie\n"
for cookie in target_cookies:
res += f" \t{await get_cookie_friendly_name(cookie)}\n"
else:
res += f" (平台 {sub.target.platform_name} 已失效,请删除此订阅)"
return res
+42
View File
@@ -1,9 +1,12 @@
import json
from typing import Literal from typing import Literal
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
import httpx
from httpx import AsyncClient from httpx import AsyncClient
from ..types import Target from ..types import Target
from ..config import config
from .http import http_client from .http import http_client
@@ -35,6 +38,45 @@ class DefaultClientManager(ClientManager):
pass pass
class CookieClientManager(ClientManager):
_platform_name: str
async def _choose_cookie(self, target: Target) -> dict[str, str]:
if not target:
return {}
cookies = await config.get_cookie_by_target(target, self._platform_name)
if not cookies:
return {}
cookie = sorted(cookies, key=lambda x: x.last_usage, reverse=True)[0]
return json.loads(cookie.content)
async def get_client(self, target: Target | None) -> AsyncClient:
client = http_client()
cookie = await self._choose_cookie(target)
cookies = httpx.Cookies()
if cookie:
cookies.update(cookie)
client.cookies = cookies
return client
async def get_client_for_static(self) -> AsyncClient:
pass
async def get_query_name_client(self) -> AsyncClient:
pass
async def refresh_client(self):
pass
def create_cookie_client_manager(platform_name: str) -> type[CookieClientManager]:
return type(
"CookieClientManager",
(CookieClientManager,),
{"_platform_name": platform_name},
)
class Site: class Site:
schedule_type: Literal["date", "interval", "cron"] schedule_type: Literal["date", "interval", "cron"]
schedule_setting: dict schedule_setting: dict
View File
+107
View File
@@ -0,0 +1,107 @@
import datetime
from nonebug import App
async def test_get_platform_target(app: App, init_scheduler):
from nonebot_plugin_saa import TargetQQGroup
from nonebot_bison.config.db_config import config
from nonebot_bison.types import Target as T_Target
await config.add_subscribe(
TargetQQGroup(group_id=123),
target=T_Target("weibo_id"),
target_name="weibo_name",
platform_name="weibo",
cats=[],
tags=[],
)
# await config.add_cookie(TargetQQGroup(group_id=123), "weibo", "cookie")
# cookies = await config.get_cookie_by_user(TargetQQGroup(group_id=123))
#
# res = await config.get_platform_target("weibo")
# assert len(res) == 2
# await config.del_subscribe(TargetQQGroup(group_id=123), T_Target("weibo_id1"), "weibo")
# res = await config.get_platform_target("weibo")
# assert len(res) == 2
# await config.del_subscribe(TargetQQGroup(group_id=123), T_Target("weibo_id"), "weibo")
# res = await config.get_platform_target("weibo")
# assert len(res) == 1
#
# async with AsyncSession(get_engine()) as sess:
# res = await sess.scalars(select(Target).where(Target.platform_name == "weibo"))
# assert len(res.all()) == 2
# await config.get_cookie_by_user(TargetQQGroup(group_id=123))
async def test_cookie_by_user(app: App, init_scheduler):
from nonebot_plugin_saa import TargetQQGroup
from nonebot_bison.config.db_config import config
from nonebot_bison.types import Target as T_Target
await config.add_subscribe(
TargetQQGroup(group_id=123),
target=T_Target("weibo_id"),
target_name="weibo_name",
platform_name="weibo",
cats=[],
tags=[],
)
await config.add_cookie(TargetQQGroup(group_id=123), "weibo", "cookie")
cookies = await config.get_cookie(TargetQQGroup(group_id=123))
cookie = cookies[0]
assert len(cookies) == 1
assert cookie.content == "cookie"
assert cookie.platform_name == "weibo"
cookie.last_usage = 0
assert cookie.status == ""
assert cookie.tags == {}
cookie.content = "cookie1"
cookie.last_usage = datetime.datetime(2024, 8, 22, 0, 0, 0)
cookie.status = "status1"
cookie.tags = {"tag1": "value1"}
await config.update_cookie(cookie)
cookies = await config.get_cookie(TargetQQGroup(group_id=123))
assert len(cookies) == 1
assert cookies[0].content == cookie.content
assert cookies[0].last_usage == cookie.last_usage
assert cookies[0].status == cookie.status
assert cookies[0].tags == cookie.tags
await config.delete_cookie(cookies[0].id)
cookies = await config.get_cookie(TargetQQGroup(group_id=123))
assert len(cookies) == 0
async def test_cookie_target_by_target(app: App, init_scheduler):
from nonebot_plugin_saa import TargetQQGroup
from nonebot_bison.config.db_config import config
from nonebot_bison.types import Target as T_Target
await config.add_subscribe(
TargetQQGroup(group_id=123),
target=T_Target("weibo_id"),
target_name="weibo_name",
platform_name="weibo",
cats=[],
tags=[],
)
id = await config.add_cookie(TargetQQGroup(group_id=123), "weibo", "cookie")
await config.add_cookie_target(T_Target("weibo_id"), "weibo", id)
cookies = await config.get_cookie_by_target(T_Target("weibo_id"), "weibo")
assert len(cookies) == 1
assert cookies[0].content == "cookie"
assert cookies[0].platform_name == "weibo"
await config.delete_cookie_target(T_Target("weibo_id"), "weibo", id)
cookies = await config.get_cookie_by_target(T_Target("weibo_id"), "weibo")
assert len(cookies) == 0
+10
View File
@@ -18,6 +18,7 @@ def pytest_configure(config: pytest.Config) -> None:
"superusers": {"10001"}, "superusers": {"10001"},
"command_start": {""}, "command_start": {""},
"log_level": "TRACE", "log_level": "TRACE",
"bison_use_browser": True,
} }
@@ -113,3 +114,12 @@ async def use_legacy_config(app: App):
# 清除单例的缓存 # 清除单例的缓存
Singleton._instances.clear() Singleton._instances.clear()
@pytest.fixture
async def _no_browser(app: App, mocker: MockerFixture):
from nonebot_bison.plugin_config import plugin_config
from nonebot_bison.platform import _get_unavailable_platforms
mocker.patch.object(plugin_config, "bison_use_browser", False)
mocker.patch("nonebot_bison.platform.unavailable_paltforms", _get_unavailable_platforms())
+8 -1
View File
@@ -183,7 +183,7 @@ async def test_retry_for_352(app: App, mocker: MockerFixture):
fakebili.set_raise352(True) fakebili.set_raise352(True)
for state in test_state_list: for state in test_state_list[:-3]:
logger.info(f"\n\nnow state should be {state}") logger.info(f"\n\nnow state should be {state}")
assert _retry_fsm.current_state == state assert _retry_fsm.current_state == state
@@ -194,6 +194,13 @@ async def test_retry_for_352(app: App, mocker: MockerFixture):
if state == RetryState.BACKOFF: if state == RetryState.BACKOFF:
freeze_start += timedelta_length * (_retry_fsm.addon.backoff_count + 1) ** 2 freeze_start += timedelta_length * (_retry_fsm.addon.backoff_count + 1) ** 2
for state in test_state_list[-3:]:
logger.info(f"\n\nnow state should be {state}")
assert _retry_fsm.current_state == state
with pytest.raises(ApiCode352Error):
await fakebili.get_sub_list(Target("t1")) # type: ignore
assert client_mgr.refresh_client_call_count == 4 * 3 + 3 # refresh + raise assert client_mgr.refresh_client_call_count == 4 * 3 + 3 # refresh + raise
assert client_mgr.get_client_call_count == 2 + 4 * 3 + 3 # previous + refresh + raise assert client_mgr.get_client_call_count == 2 + 4 * 3 + 3 # previous + refresh + raise
+18 -4
View File
@@ -88,9 +88,21 @@ async def test_fetch_new_1(
assert post1.title is None assert post1.title is None
assert ( assert (
post1.content post1.content
== "【#統合戦略】 引き続き新テーマ「ミヅキと紺碧の樹」の新要素及びシステムの変更点を一部ご紹介します!" == "【#統合戦略】 <br />引き続き新テーマ「ミヅキと紺碧の樹」の新要素及びシステムの変更点を一部ご紹介します! "
" 今回は「灯火」、「ダイス」、「記号認識」、「鍵」についてです。詳細は添付の画像をご確認ください。" "<br /><br />"
"#アークナイツ https://t.co/ARmptV0Zvu" "今回は「灯火」、「ダイス」、「記号認識」、「鍵」についてです。<br />詳細は添付の画像をご確認ください。"
"<br /><br />"
"#アークナイツ https://t.co/ARmptV0Zvu<br />"
'<img src="https://pbs.twimg.com/media/FwZG9YAacAIXDw2?format=jpg&amp;name=orig" />'
)
plain_content = await post1.get_plain_content()
assert (
plain_content == "【#統合戦略】 \n"
"引き続き新テーマ「ミヅキと紺碧の樹」の新要素及びシステムの変更点を一部ご紹介します! \n\n"
"今回は「灯火」、「ダイス」、「記号認識」、「鍵」についてです。\n"
"詳細は添付の画像をご確認ください。\n\n"
"#アークナイツ https://t.co/ARmptV0Zvu\n"
"[图片]"
) )
@@ -174,7 +186,9 @@ async def test_fetch_new_4(
assert len(res2[0][1]) == 1 assert len(res2[0][1]) == 1
post1 = res2[0][1][0] post1 = res2[0][1][0]
assert post1.url == "https://wallhaven.cc/w/85rjej" assert post1.url == "https://wallhaven.cc/w/85rjej"
assert post1.content == "85rjej.jpg" assert post1.content == '<img alt="loading" class="lazyload" src="https://th.wallhaven.cc/small/85/85rjej.jpg" />'
plain_content = await post1.get_plain_content()
assert plain_content == "[图片]"
def test_similar_text_process(): def test_similar_text_process():
+45
View File
@@ -615,3 +615,48 @@ async def test_add_with_bilibili_bangumi_target_parser(app: App, init_scheduler)
assert sub.tags == [] assert sub.tags == []
assert sub.target.platform_name == "bilibili-bangumi" assert sub.target.platform_name == "bilibili-bangumi"
assert sub.target.target_name == "汉化日记 第三季" assert sub.target.target_name == "汉化日记 第三季"
@pytest.mark.asyncio
async def test_subscribe_platform_requires_browser(app: App, mocker: MockerFixture):
from nonebot.adapters.onebot.v11.event import Sender
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.plugin_config import plugin_config
from nonebot_bison.sub_manager import add_sub_matcher, common_platform
from nonebot_bison.platform import platform_manager, unavailable_paltforms
mocker.patch.object(plugin_config, "bison_use_browser", False)
mocker.patch.dict(unavailable_paltforms, {"bilibili": "需要启用 bison_use_browser"})
async with app.test_matcher(add_sub_matcher) as ctx:
bot = ctx.create_bot()
event_1 = fake_group_message_event(
message=Message("添加订阅"),
sender=Sender(card="", nickname="test", role="admin"),
to_me=True,
)
ctx.receive_event(bot, event_1)
ctx.should_pass_rule()
ctx.should_call_send(
event_1,
BotReply.add_reply_on_platform(platform_manager=platform_manager, common_platform=common_platform),
True,
)
event_2 = fake_group_message_event(
message=Message("全部"), sender=Sender(card="", nickname="test", role="admin")
)
ctx.receive_event(bot, event_2)
ctx.should_rejected()
ctx.should_call_send(
event_2,
BotReply.add_reply_on_platform_input_allplatform(platform_manager),
True,
)
event_3 = fake_group_message_event(message=Message("bilibili"), sender=fake_admin_user)
ctx.receive_event(bot, event_3)
ctx.should_call_send(
event_3,
BotReply.add_reply_platform_unavailable("bilibili", "需要启用 bison_use_browser"),
True,
)
+45
View File
@@ -0,0 +1,45 @@
import pytest
from nonebug import App
from ..utils import BotReply, fake_admin_user, fake_group_message_event
@pytest.mark.asyncio
async def test_with_permission(app: App):
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.platform import platform_manager
from nonebot_bison.sub_manager import add_sub_matcher, common_platform, no_permission_matcher
async with app.test_matcher([add_sub_matcher, no_permission_matcher]) as ctx:
bot = ctx.create_bot(base=Bot)
event = fake_group_message_event(message=Message("添加订阅"), sender=fake_admin_user, to_me=True)
ctx.receive_event(bot, event)
ctx.should_call_send(
event,
BotReply.add_reply_on_platform(platform_manager, common_platform),
True,
)
ctx.should_pass_rule()
ctx.should_pass_permission()
@pytest.mark.asyncio
async def test_without_permission(app: App):
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebot_bison.sub_manager import add_sub_matcher, no_permission_matcher
async with app.test_matcher([add_sub_matcher, no_permission_matcher]) as ctx:
bot = ctx.create_bot(base=Bot)
event = fake_group_message_event(message=Message("添加订阅"), to_me=True)
ctx.receive_event(bot, event)
ctx.should_call_send(
event,
BotReply.no_permission,
True,
)
ctx.should_pass_rule()
ctx.should_pass_permission()
+5
View File
@@ -146,6 +146,10 @@ class BotReply:
extra_text = ("1." + target_promot + "\n2.") if target_promot else "" extra_text = ("1." + target_promot + "\n2.") if target_promot else ""
return extra_text + base_text return extra_text + base_text
@staticmethod
def add_reply_platform_unavailable(platform: str, reason: str) -> str:
return f"无法订阅 {platform}{reason}"
add_reply_on_id_input_error = "id输入错误" add_reply_on_id_input_error = "id输入错误"
add_reply_on_target_parse_input_error = "不能从你的输入中提取出id,请检查你输入的内容是否符合预期" add_reply_on_target_parse_input_error = "不能从你的输入中提取出id,请检查你输入的内容是否符合预期"
add_reply_on_platform_input_error = "平台输入错误" add_reply_on_platform_input_error = "平台输入错误"
@@ -154,3 +158,4 @@ class BotReply:
) )
add_reply_on_tags_need_more_info = "订阅标签直接输入标签内容\n屏蔽标签请在标签名称前添加~号\n详见https://nonebot-bison.netlify.app/usage/#%E5%B9%B3%E5%8F%B0%E8%AE%A2%E9%98%85%E6%A0%87%E7%AD%BE-tag" add_reply_on_tags_need_more_info = "订阅标签直接输入标签内容\n屏蔽标签请在标签名称前添加~号\n详见https://nonebot-bison.netlify.app/usage/#%E5%B9%B3%E5%8F%B0%E8%AE%A2%E9%98%85%E6%A0%87%E7%AD%BE-tag"
add_reply_abort = "已中止订阅" add_reply_abort = "已中止订阅"
no_permission = "您没有权限进行此操作,请联系 Bot 管理员"