9 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
suyiiyii 1cd778c2e0 ♻️ cookie 组件不再与 user 关联 2024-08-31 23:07:43 +08:00
suyiiyii c828fd94e4 ♻️ cookie 组件不再与 user 关联 2024-08-31 23:04:49 +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
31 changed files with 3911 additions and 3937 deletions
+1 -1
View File
@@ -4,7 +4,7 @@
"features": {
"ghcr.io/devcontainers-contrib/features/poetry:2": {}
},
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && npm install -g pnpm && pnpm install",
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && yarn install",
"customizations": {
"vscode": {
"settings": {
+2 -2
View File
@@ -10,5 +10,5 @@ runs:
- name: Build Frontend
shell: bash
run: |
pnpm install
pnpm docs:build
yarn install
yarn docs:build
+2 -2
View File
@@ -11,5 +11,5 @@ runs:
shell: bash
working-directory: ./admin-frontend
run: |
pnpm install
pnpm build
yarn install
yarn build
+4 -9
View File
@@ -8,16 +8,11 @@ runs:
with:
node-version: "20"
- name: Set Up Pnpm
uses: pnpm/action-setup@v4
with:
version: 9
- id: pnpm-cache-dir-path
run: echo "::set-output name=dir::$(pnpm store path)"
- id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
shell: bash
- uses: actions/cache@v4
with:
path: ${{ steps.pnpm-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
+2 -2
View File
@@ -7,7 +7,7 @@ ci:
autoupdate_commit_msg: ":arrow_up: auto update by pre-commit hooks"
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.3
rev: v0.6.0
hooks:
- id: ruff
args: [--fix, --exit-non-zero-on-fix]
@@ -34,7 +34,7 @@ repos:
stages: [commit]
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v9.9.1
rev: v9.8.0
hooks:
- id: eslint
additional_dependencies:
+1 -2
View File
@@ -1,10 +1,9 @@
# Change Log
## v0.9.4
## 最近更新
### Bug 修复
- FSM 内部执行外部函数出现异常时不应崩溃 [@AzideCupric](https://github.com/AzideCupric) ([#616](https://github.com/MountainDash/nonebot-bison/pull/616))
- 无权限用户尝试添加订阅时返回提示信息 [@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))
+18 -18
View File
@@ -5,26 +5,26 @@
"homepage": "bison",
"proxy": "http://127.0.0.1:8080",
"dependencies": {
"@arco-design/web-react": "^2.64.0",
"@babel/core": "^7.25.2",
"@arco-design/web-react": "^2.63.1",
"@babel/core": "^7.24.7",
"@babel/plugin-syntax-flow": "^7.24.7",
"@babel/plugin-transform-react-jsx": "^7.25.2",
"@babel/plugin-transform-react-jsx": "^7.24.7",
"@reduxjs/toolkit": "^1.9.7",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.1",
"@testing-library/react": "^16.0.0",
"@testing-library/user-event": "^14.5.2",
"@types/jest": "^29.5.13",
"@types/node": "^20.16.5",
"@types/react": "^18.3.7",
"@types/jest": "^29.5.12",
"@types/node": "^20.14.10",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-redux": "^9.1.2",
"react-router-dom": "^6.26.2",
"react-router-dom": "^6.24.1",
"react-scripts": "5.0.1",
"redux": "^5.0.1",
"redux-persist": "^6.0.0",
"typescript": "^5.6.2",
"typescript": "^5.5.4",
"web-vitals": "^3.5.2"
},
"scripts": {
@@ -53,17 +53,17 @@
]
},
"devDependencies": {
"@testing-library/jest-dom": "^6.5.0",
"@typescript-eslint/eslint-plugin": "^8.6.0",
"@typescript-eslint/parser": "^8.6.0",
"eslint": "^8.57.1",
"@testing-library/jest-dom": "^6.4.6",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.6.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^18.0.0",
"eslint-import-resolver-typescript": "^3.6.3",
"eslint-plugin-import": "^2.30.0",
"eslint-plugin-jsx-a11y": "^6.10.0",
"eslint-plugin-react": "^7.36.1",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-jsx-a11y": "^6.9.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-react-redux": "^4.2.0"
"eslint-plugin-react-redux": "^4.1.0"
}
}
+1034 -1046
View File
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -1,8 +1,7 @@
FROM node:20.17.0 as frontend
FROM node:20.15.1 as frontend
ADD . /app
WORKDIR /app/admin-frontend
RUN npm install -g pnpm
RUN pnpm install && pnpm build
RUN yarn && yarn build
FROM python:3.11
RUN python3 -m pip install poetry && poetry config virtualenvs.create false
+1 -1
View File
@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.10
# syntax=docker/dockerfile:1.8
FROM python:3.11-slim-bullseye as base
FROM base as builder
+1 -1
View File
@@ -1,4 +1,4 @@
# syntax=docker/dockerfile:1.10
# syntax=docker/dockerfile:1.8
FROM python:3.11-slim-bullseye as base
FROM base as builder
+11
View File
@@ -1,4 +1,5 @@
from .types import Target
from .config.db_model import Cookie
from .scheduler import scheduler_dict
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()
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 Target as T_Target
from .utils import NoSuchTargetException
from .db_model import User, Target, Subscribe, ScheduleTimeWeight
from .utils import NoSuchTargetException, DuplicateCookieTargetException
from .db_model import User, Cookie, Target, Subscribe, CookieTarget, ScheduleTimeWeight
from ..types import Category, UserSubInfo, WeightConfig, TimeWeightConfig, PlatformWeightConfigResp
@@ -259,5 +259,79 @@ class DBConfig:
)
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()
+23 -1
View File
@@ -1,4 +1,5 @@
import datetime
from typing import Any
from pathlib import Path
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_plugin_datastore import get_plugin_data
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
@@ -36,6 +37,7 @@ class Target(Model):
subscribes: Mapped[list["Subscribe"]] = relationship(back_populates="target")
time_weight: Mapped[list["ScheduleTimeWeight"]] = relationship(back_populates="target")
cookies: Mapped[list["CookieTarget"]] = relationship(back_populates="target")
class ScheduleTimeWeight(Model):
@@ -66,3 +68,23 @@ class Subscribe(Model):
target: Mapped[Target] = 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):
pass
class DuplicateCookieTargetException(Exception):
pass
+1 -62
View File
@@ -2,24 +2,10 @@ import sys
import asyncio
import inspect
from enum import Enum
from functools import wraps
from dataclasses import dataclass
from collections.abc import Set as AbstractSet
from collections.abc import Callable, Sequence, Awaitable, AsyncGenerator
from typing import (
TYPE_CHECKING,
Any,
Generic,
TypeVar,
Protocol,
ParamSpec,
TypeAlias,
TypedDict,
NamedTuple,
Concatenate,
overload,
runtime_checkable,
)
from typing import TYPE_CHECKING, Any, Generic, TypeVar, Protocol, TypeAlias, TypedDict, NamedTuple, runtime_checkable
from nonebot import logger
@@ -31,7 +17,6 @@ TAddon = TypeVar("TAddon", contravariant=True)
TState = TypeVar("TState", contravariant=True)
TEvent = TypeVar("TEvent", contravariant=True)
TFSM = TypeVar("TFSM", bound="FSM", contravariant=True)
P = ParamSpec("P")
class StateError(Exception): ...
@@ -178,52 +163,6 @@ class FSM(Generic[TState, TEvent, TAddon]):
self.started = False
del self.machine
self.current_state = self.graph["initial"]
self.machine = self._core()
logger.trace("FSM closed")
@overload
def reset_on_exception(
func: Callable[Concatenate[TFSM, P], Awaitable[ActionReturn]],
) -> Callable[Concatenate[TFSM, P], Awaitable[ActionReturn]]:
"""自动在发生异常后重置 FSM"""
@overload
def reset_on_exception(
auto_start: bool = False,
) -> Callable[
[Callable[Concatenate[TFSM, P], Awaitable[ActionReturn]]], Callable[Concatenate[TFSM, P], Awaitable[ActionReturn]]
]:
"""自动在异常后重置 FSM,当 auto_start 为 True 时,自动启动 FSM"""
# 参考自 dataclasses.dataclass 的实现
def reset_on_exception(func=None, /, *, auto_start=False): # pyright: ignore[reportInconsistentOverload]
def warp(func: Callable[Concatenate[TFSM, P], Awaitable[ActionReturn]]):
return __reset_clear_up(func, auto_start)
# 判断调用的是 @reset_on_exception 还是 @reset_on_exception(...)
if func is None:
# 调用的是带括号的
return warp
# 调用的是不带括号的
return warp(func)
def __reset_clear_up(func: Callable[Concatenate[TFSM, P], Awaitable[ActionReturn]], auto_start: bool):
@wraps(func)
async def wrapper(fsm_self: TFSM, *args: P.args, **kwargs: P.kwargs) -> ActionReturn:
try:
return await func(fsm_self, *args, **kwargs)
except Exception as e:
logger.error(f"Exception in {func.__name__}: {e}")
await fsm_self.reset()
if auto_start and not fsm_self.started:
await fsm_self.start()
raise e
return wrapper
+1 -6
View File
@@ -14,7 +14,7 @@ from httpx import URL as HttpxURL
from nonebot_bison.types import Target
from .models import DynRawPost
from .fsm import FSM, Condition, StateGraph, Transition, ActionReturn, reset_on_exception
from .fsm import FSM, Condition, StateGraph, Transition, ActionReturn
if TYPE_CHECKING:
from .platforms import Bilibili
@@ -218,11 +218,6 @@ class RetryFSM(FSM[RetryState, RetryEvent, RetryAddon[TBilibili]]):
self.addon.reset_all()
await super().reset()
@override
@reset_on_exception
async def emit(self, event: RetryEvent):
await super().emit(event)
# FIXME: 拿出来是方便测试了,但全局单例会导致所有被装饰的函数共享状态,有待改进
_retry_fsm = RetryFSM(RETRY_GRAPH, RetryAddon["Bilibili"]())
+3 -1
View File
@@ -10,12 +10,14 @@ from ..post import Post
from .platform import NewMessage
from ..types import Target, RawPost
from ..utils import Site, text_similarity
from ..utils.site import create_cookie_client_manager
class RssSite(Site):
name = "rss"
schedule_type = "interval"
schedule_setting = {"seconds": 30}
client_mgr = create_cookie_client_manager("rss")
class RssPost(Post):
@@ -63,7 +65,7 @@ class Rss(NewMessage):
return post.id
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)
feed = feedparser.parse(res)
entries = feed.entries
+22 -2
View File
@@ -14,6 +14,8 @@ from nonebot.adapters.onebot.v11.event import PrivateMessageEvent
from .add_sub import do_add_sub
from .del_sub import do_del_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
add_sub_matcher = on_command(
@@ -26,12 +28,10 @@ add_sub_matcher = on_command(
add_sub_matcher.handle()(set_target_user_info)
do_add_sub(add_sub_matcher)
query_sub_matcher = on_command("查询订阅", rule=configurable_to_me, priority=5, block=True)
query_sub_matcher.handle()(set_target_user_info)
do_query_sub(query_sub_matcher)
del_sub_matcher = on_command(
"删除订阅",
rule=configurable_to_me,
@@ -42,6 +42,26 @@ del_sub_matcher = on_command(
del_sub_matcher.handle()(set_target_user_info)
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_handle_cancel = gen_handle_cancel(group_manage_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']}"
)
+49 -1
View File
@@ -1,16 +1,21 @@
import contextlib
from typing import Annotated
from itertools import groupby
from operator import attrgetter
from nonebot.rule import Rule
from nonebot.adapters import Event
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.permission import SUPERUSER
from nonebot_plugin_saa import extract_target
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 ..plugin_config import plugin_config
from ..apis import get_cookie_friendly_name
def _configurable_to_me(to_me: bool = EventToMe()):
@@ -60,3 +65,46 @@ def admin_permission():
permission = permission | GROUP_ADMIN | GROUP_OWNER
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 abc import ABC, abstractmethod
import httpx
from httpx import AsyncClient
from ..types import Target
from ..config import config
from .http import http_client
@@ -35,6 +38,45 @@ class DefaultClientManager(ClientManager):
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:
schedule_type: Literal["date", "interval", "cron"]
schedule_setting: dict
+4 -4
View File
@@ -11,9 +11,9 @@
"docs:update-package": "pnpm dlx vp-update"
},
"devDependencies": {
"@vuepress/bundler-vite": "2.0.0-rc.15",
"vue": "^3.5.6",
"vuepress": "2.0.0-rc.15",
"vuepress-theme-hope": "2.0.0-rc.52"
"@vuepress/bundler-vite": "2.0.0-rc.14",
"vue": "^3.4.31",
"vuepress": "2.0.0-rc.14",
"vuepress-theme-hope": "2.0.0-rc.50"
}
}
+1771 -2121
View File
File diff suppressed because it is too large Load Diff
Generated
+519 -536
View File
File diff suppressed because it is too large Load Diff
+24 -24
View File
@@ -1,6 +1,6 @@
[tool.poetry]
name = "nonebot-bison"
version = "0.9.4"
version = "0.9.3"
description = "Subscribe message from social medias"
authors = ["felinae98 <felinae225@qq.com>"]
license = "MIT"
@@ -24,40 +24,40 @@ classifiers = [
python = ">=3.10,<4.0.0"
beautifulsoup4 = ">=4.12.3"
feedparser = "^6.0.11"
httpx = ">=0.27.2"
nonebot2 = { extras = ["fastapi"], version = "^2.3.3" }
nonebot-adapter-onebot = "^2.4.5"
nonebot-plugin-htmlrender = ">=0.3.5"
httpx = ">=0.27.0"
nonebot2 = { extras = ["fastapi"], version = "^2.3.2" }
nonebot-adapter-onebot = "^2.4.4"
nonebot-plugin-htmlrender = ">=0.3.3"
nonebot-plugin-datastore = ">=1.3.0,<2.0.0"
nonebot-plugin-apscheduler = ">=0.5.0"
nonebot-plugin-send-anything-anywhere = ">=0.7.1,<0.7.2"
pillow = ">=10.4.0,<11.0"
pyjwt = "^2.9.0"
python-socketio = "^5.11.4"
nonebot-plugin-send-anything-anywhere = ">=0.6.1,<0.7.0"
pillow = ">=8.4.0,<11.0"
pyjwt = "^2.8.0"
python-socketio = "^5.11.3"
tinydb = "^4.8.0"
qrcode = "^7.4.2"
pydantic = ">=2.9.2,<3.0.0,!=2.5.0,!=2.5.1"
lxml = ">=5.3.0"
yarl = ">=1.11.1"
hishel = "^0.0.30"
expiringdictx = "^1.1.0"
rapidfuzz = "^3.9.7"
pydantic = ">=1.10.17,<3.0.0,!=2.5.0,!=2.5.1"
lxml = ">=5.2.2"
yarl = ">=1.9.4"
hishel = "^0.0.20"
expiringdictx = "^1.0.1"
rapidfuzz = "^3.9.3"
[tool.poetry.group.dev.dependencies]
black = ">=24.8.0,<25.0"
black = ">=23.12.1,<25.0"
ipdb = "^0.13.13"
isort = "^5.13.2"
nonemoji = "^0.1.4"
nb-cli = "^1.4.2"
pre-commit = "^3.8.0"
ruff = "^0.6.5"
nb-cli = "^1.4.1"
pre-commit = "^3.7.1"
ruff = "^0.6.0"
[tool.poetry.group.test.dependencies]
flaky = "^3.8.1"
nonebug = "^0.3.7"
nonebug-saa = "^0.4.1"
pytest = ">=8.3.3,<9.0.0"
pytest-asyncio = ">=0.24.0,<0.24.1"
pytest = ">=7.4.4,<9.0.0"
pytest-asyncio = ">=0.23.7,<0.24.0"
pytest-cov = ">=5.0.0,<6"
pytest-mock = "^3.14.0"
pytest-xdist = { extras = ["psutil"], version = "^3.6.1" }
@@ -68,10 +68,10 @@ freezegun = "^1.5.1"
optional = true
[tool.poetry.group.docker.dependencies]
nb-cli = "^1.4.2"
nonebot2 = { extras = ["fastapi", "aiohttp"], version = "^2.3.3" }
nb-cli = "^1.4.1"
nonebot2 = { extras = ["fastapi", "aiohttp"], version = "^2.3.2" }
nonebot-adapter-red = "^0.9.0"
nonebot-adapter-qq = "^1.5.1"
nonebot-adapter-qq = "^1.4.4"
poetry-core = "^1.9.0"
[tool.poetry.extras]
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
-86
View File
@@ -58,92 +58,6 @@ def without_dynamic(app: App):
)
@pytest.mark.asyncio
async def test_reset_on_exception(app: App):
from strenum import StrEnum
from nonebot_bison.platform.bilibili.fsm import FSM, StateGraph, Transition, ActionReturn, reset_on_exception
class State(StrEnum):
A = "A"
B = "B"
C = "C"
class Event(StrEnum):
A = "A"
B = "B"
C = "C"
class Addon:
pass
async def raction(from_: State, event: Event, to: State, addon: Addon) -> ActionReturn:
logger.info(f"action: {from_} -> {to}")
raise RuntimeError("test")
async def action(from_: State, event: Event, to: State, addon: Addon) -> ActionReturn:
logger.info(f"action: {from_} -> {to}")
graph: StateGraph[State, Event, Addon] = {
"transitions": {
State.A: {
Event.A: Transition(raction, State.B),
Event.B: Transition(action, State.C),
},
State.B: {
Event.B: Transition(action, State.C),
},
State.C: {
Event.C: Transition(action, State.A),
},
},
"initial": State.A,
}
addon = Addon()
class AFSM(FSM[State, Event, Addon]):
@reset_on_exception(auto_start=True)
async def emit(self, event: Event):
return await super().emit(event)
fsm = AFSM(graph, addon)
await fsm.start()
with pytest.raises(RuntimeError):
await fsm.emit(Event.A)
assert fsm.started is True
await fsm.emit(Event.B)
await fsm.emit(Event.C)
class BFSM(FSM[State, Event, Addon]):
@reset_on_exception
async def emit(self, event: Event):
return await super().emit(event)
fsm = BFSM(graph, addon)
await fsm.start()
with pytest.raises(RuntimeError):
await fsm.emit(Event.A)
assert fsm.started is False
with pytest.raises(TypeError, match="can't send non-None value to a just-started async generator"):
await fsm.emit(Event.B)
class CFSM(FSM[State, Event, Addon]): ...
fsm = CFSM(graph, addon)
await fsm.start()
with pytest.raises(RuntimeError):
await fsm.emit(Event.A)
assert fsm.started is True
with pytest.raises(StopAsyncIteration):
await fsm.emit(Event.B)
@pytest.mark.asyncio
async def test_retry_for_352(app: App, mocker: MockerFixture):
from nonebot_bison.post import Post