Compare commits

..

96 Commits

Author SHA1 Message Date
40f4490b74
🔀 merge main 2024-10-31 00:50:20 +08:00
github-actions[bot]
3bdc79162e 🔖 Release 0.9.5 2024-10-30 15:54:06 +00:00
felinae98
8f86f61802
🔖 release 0.9.5 2024-10-30 23:51:14 +08:00
github-actions[bot]
ab5154d523 📝 Update changelog 2024-10-30 15:50:12 +00:00
洛梧藤
77a4dcd70e
🐛 修复微博更换长内容接口 (#645) 2024-10-30 23:49:39 +08:00
a1843bb98a
🐛 优化 SiteMeta 2024-10-30 23:43:18 +08:00
f08ab9f926
📝 fixs
📝 删除开发文档中过多的人称代词
2024-10-30 23:24:13 +08:00
fd349eefed
♻️ 复原 RegistryMeta 的位置 2024-10-30 20:18:28 +08:00
e8f0d578e1
♻️ 将 Site 的元类从 RegistryMeta 改为 新建的 SiteMeta 2024-10-30 20:03:48 +08:00
60dd2c4bab
📝 同步更新文档 2024-10-29 23:22:27 +08:00
d6b8d3b44e
✏️ small fixs 2024-10-29 23:13:26 +08:00
3f7a9bf8a3
🔀 Merge remote-tracking branch 'upstream/main' into cookie 2024-10-29 22:56:10 +08:00
955a06d9e9
🐛 修复低版本 python 不支持 override 2024-10-29 22:19:03 +08:00
074fe3ff58
♻️ _site_name 和 _default_cookie_cd 内置 CookieClientManager 2024-10-29 21:28:35 +08:00
8a8a48aef6
♻️ 移除CookieSite 2024-10-29 21:17:07 +08:00
c5dea7e252
♻️ 初步移除CookieSite 2024-10-29 21:12:50 +08:00
07190a7f64
♻️ 改回 _site_name 2024-10-29 20:53:23 +08:00
62386931d7
♻️ 移除 bilibili 的 _site_name 属性 2024-10-29 14:43:16 +08:00
32c237015f
♻️ 更新部分方法使用 scheduler_dict 进行cookie操作 2024-10-29 14:15:15 +08:00
29c2eb456d
♻️ 更新部分方法使用 scheduler_dict 进行cookie操作 2024-10-29 12:24:31 +08:00
9d985eb3c8
♻️ bilibili 移除 _client 属性 2024-10-29 12:23:50 +08:00
4805d0d77c
♻️ 在 CookieClientManager 中添加对对应的 Site 的引用 2024-10-29 11:33:28 +08:00
81e53419a3
♻️ 将 refresh_anonymous_cookie 改为内部方法, 同时外部使用 refresh_client 方法进行刷新匿名 cookie 2024-10-29 09:38:08 +08:00
b130627d7e
♻️ 还原对 bilibili platform 的修改 2024-10-29 00:48:35 +08:00
4dd4555fc7
📝 更新文档 2024-10-29 00:44:58 +08:00
e6c45e5b1b
♻️ 为 row2dict 添加类型注解 2024-10-28 23:56:10 +08:00
b60ba566de
♻️ 将 default_cd 重命名为 default_cookie_cd 2024-10-28 23:54:12 +08:00
ac794efd18
:refactor: 将 cookie default_cd 移动到 CookieSite 内部 2024-10-28 23:50:04 +08:00
cdd671b15f
🐛 修复「管理后台」重复发送消息 2024-10-28 23:06:54 +08:00
6e53c6f4b2
🐛 优化对 bilibili cookie 的 mock 2024-10-28 22:48:11 +08:00
3a0f95b712
🐛 优化对 weibo get_cookie_name 的 mock 2024-10-28 22:47:31 +08:00
641cc44a12
🔀 merge 2024-10-28 22:07:20 +08:00
42cc56ac24
🐛 优化对 bilibili cookie 的 mock 2024-10-28 22:06:12 +08:00
0deb406692
🐛 更新判断superuser的私聊消息的逻辑 2024-10-28 22:00:15 +08:00
github-actions[bot]
d4f45571b3 📝 Update changelog 2024-10-28 13:47:35 +00:00
a671bd0c61
🐛 修复B站获取匿名Cookie逻辑 (#644) 2024-10-28 21:47:04 +08:00
eb64eab14a
添加cookie命令的无权限提示 2024-10-28 13:46:22 +08:00
3043817de4
🐛 为cookie相关操作添加非私聊拒绝提示 2024-10-28 13:29:10 +08:00
0985705c22
🐛 添加 Cookie 时,显示 「无法获取cookie_name」 的错误提示 2024-10-28 13:17:36 +08:00
438b23a0b9
🐛 从 Site 中移除 「无效的 Cookie」 文本 2024-10-28 12:44:51 +08:00
dcd32f0662
📝 更新文档顶栏 2024-10-28 12:37:17 +08:00
60cee1bcc1
🐛 修复B站获取匿名Cookie逻辑-ExClimbWuzhi 2024-10-24 21:40:32 +08:00
e64fb09145
🐛 修复B站获取匿名Cookie逻辑 2024-10-24 17:43:13 +08:00
github-actions[bot]
08ba7b0e3b 📝 Update changelog 2024-10-24 08:56:51 +00:00
1a0eca4a38
更新默认UA为Windows平台 (#643) 2024-10-24 16:56:24 +08:00
d2c33feff8
mock生成匿名Cookie的逻辑 2024-10-23 10:53:11 +08:00
ef52339337
移除用于测试的 token 2024-10-22 21:54:04 +08:00
d614d8bf64
为 bilibili 初步适配Cookie功能 2024-10-22 21:15:37 +08:00
e84894cd9e
CookieClientManager中提出_generate_anonymous_cookie方法 2024-10-22 21:13:31 +08:00
ff1c3c3159
WebUI 中,允许查看 Cookie 的 content 2024-10-21 20:58:21 +08:00
8d32145ea8
✏️ 改改变量名 2024-10-21 20:43:08 +08:00
f5d30b998c
📝 添加 获取Cookie 的文档 2024-10-21 13:44:46 +08:00
a090ff2127
Merge branch 'MountainDash:main' into cookie 2024-10-21 13:21:44 +08:00
6474504c30
(admin) 添加验证Cookie有效性 2024-10-21 11:22:54 +08:00
a78bb73281
(admin) 添加验证Cookie有效性接口 2024-10-21 10:04:00 +08:00
c3289671d8
📝 关于 Cookie 的开发指南 2024-10-19 13:25:07 +08:00
0f4b0aab86
📝 好像还是要加上sass-embedded这个库 2024-10-16 18:15:07 +08:00
69ef94bcf5
📝 破防了,不整zenuml了 2024-10-16 17:14:59 +08:00
3c090db0cb
📝 似乎可以了? 2024-10-16 10:41:36 +08:00
56627753fc
📝 怎么前端编译报错了( 2024-10-16 09:35:59 +08:00
7359b3ef9d
📝 怎么前端编译报错了( 2024-10-16 09:33:59 +08:00
7622d3da1e
📝 好像可以用 zenuml 了 ) 2024-10-15 21:43:25 +08:00
3773c77864
📝 试试zenuml ( 2024-10-15 18:12:53 +08:00
cdc8de9619
📝 试试流程图) 2024-10-14 15:35:29 +08:00
7aac134d5d
📝 测试mermaid画图 2024-10-14 12:32:59 +08:00
b2b20ab7c5
📝 添加cookie模块使用文档 2024-10-14 01:14:03 +08:00
2093622672
🐛 改改单测 2024-10-13 21:51:47 +08:00
b6ba904a68
🐛 又忘记改单测了( 2024-10-13 21:30:08 +08:00
f3d8b7d5bc
添加 cookie 时自动使用用户名命名(weibo) 2024-10-13 20:44:37 +08:00
b1b8d37171
(admin) console.log()忘记删了 ( 2024-10-12 13:41:28 +08:00
271e3e7a72
🗑️ 删除旧的CookieTargetManager 2024-10-12 13:37:17 +08:00
a941d77257
(admin) 支持删除cookieTarget( 2024-10-12 13:35:53 +08:00
6d8de1b59e
(admin) 支持添加cookieTarget 2024-10-12 13:14:04 +08:00
8174ec895d (admin) 支持查看cookie详情 2024-10-11 13:41:00 +08:00
10bb6179ae (admin) 支持查看cookie详情 2024-10-11 13:40:31 +08:00
7030758ac9 (admin) 支持删除cookie 2024-10-11 12:05:28 +08:00
a18b2e4f7b (admin) 支持添加cookie 2024-10-11 11:54:30 +08:00
3f3cc2d25e 🐛 更改命名siteName以符合小驼峰规范 2024-10-11 11:37:18 +08:00
5b8d0440ee (admin) 完善 将site选择移动到侧边栏 2024-10-11 11:12:15 +08:00
24251c2728 (admin) 将site选择移动到侧边栏 2024-10-09 23:23:43 +08:00
19345cf960
移动validate_cookie到CookieSite中 2024-10-09 23:18:43 +08:00
1346d07982
修改cookie_site.get_cookie_name为异步 2024-10-09 23:14:17 +08:00
renovate[bot]
ce1c902905
⬆️ Update dependency pre-commit to v4 (#637)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-10-09 14:19:06 +08:00
6c0efdddfc 🔀 merge main 2024-10-08 17:24:34 +08:00
洛初
40f9bc817f
feat(ISSUE_TEMPLATE): 修改问题报告模板 (#630)
Some checks failed
test-build / Build Frontend (push) Has been cancelled
test-build / Smoke-test Coverage (macos-latest, 3.10) (push) Has been cancelled
test-build / Smoke-test Coverage (macos-latest, 3.11) (push) Has been cancelled
test-build / Smoke-test Coverage (macos-latest, 3.12) (push) Has been cancelled
test-build / Smoke-test Coverage (ubuntu-latest, 3.10) (push) Has been cancelled
test-build / Smoke-test Coverage (ubuntu-latest, 3.11) (push) Has been cancelled
test-build / Smoke-test Coverage (ubuntu-latest, 3.12) (push) Has been cancelled
test-build / Smoke-test Coverage (windows-latest, 3.10) (push) Has been cancelled
test-build / Smoke-test Coverage (windows-latest, 3.11) (push) Has been cancelled
test-build / Smoke-test Coverage (windows-latest, 3.12) (push) Has been cancelled
test-build / All-test Coverage (macos-latest, 3.10) (push) Has been cancelled
test-build / All-test Coverage (macos-latest, 3.11) (push) Has been cancelled
test-build / All-test Coverage (macos-latest, 3.12) (push) Has been cancelled
test-build / All-test Coverage (ubuntu-latest, 3.10) (push) Has been cancelled
test-build / All-test Coverage (ubuntu-latest, 3.11) (push) Has been cancelled
test-build / All-test Coverage (ubuntu-latest, 3.12) (push) Has been cancelled
test-build / All-test Coverage (windows-latest, 3.10) (push) Has been cancelled
test-build / All-test Coverage (windows-latest, 3.11) (push) Has been cancelled
test-build / All-test Coverage (windows-latest, 3.12) (push) Has been cancelled
pydantic1-compat-test / pydantic1 test (ubuntu-latest, 3.11) (push) Has been cancelled
Ruff Lint / Ruff Lint (push) Has been cancelled
test-build / Docker main (push) Has been cancelled
test-build / Docker main sentry (push) Has been cancelled
*  feat(ISSUE_TEMPLATE): 修改问题报告模板 

  feat(ISSUE_TEMPLATE): 修改问题报告模板

* 💄 auto fix by pre-commit hooks

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-03 10:31:47 +08:00
github-actions[bot]
f416b249f7 📝 Update changelog 2024-10-03 02:31:09 +00:00
ec6fe2679d
👷 将自动化工具中的 yarn 替换为 pnpm (#634)
* 👷 将自动化工具中的 yarn 替换为 pnpm

* 🐛 写串台了艹

* 🐛 最讨厌调试 action 了 (
2024-10-03 10:29:57 +08:00
github-actions[bot]
19b25552d0 📝 Update changelog 2024-10-01 07:38:45 +00:00
洛梧藤
85b5ab3868
📝 小刻食堂剪彩文档 (#636) 2024-10-01 15:38:16 +08:00
1d1b9f6574 🐛 config 中添加 clear_db 方法,用于清空数据库内容;添加clear_db fixture,用于在单测前后清空数据库 2024-09-28 16:51:22 +08:00
aa897939d9 为什么 eslint 的warning都不让我过编译 ( 2024-09-27 09:51:19 +08:00
7d3193d958 为什么前端 pnpm 和 yarn 的行为还不一样( 2024-09-27 01:18:59 +08:00
f31a798326 修修单测 2024-09-27 00:45:45 +08:00
pre-commit-ci[bot]
6b6bf9d8f8 💄 auto fix by pre-commit hooks 2024-09-26 11:41:12 +00:00
54d37e254d 📝 尝试添加文档 2024-09-26 19:38:43 +08:00
8fa456bc4e 📝 尝试添加文档 2024-09-26 19:37:06 +08:00
72 changed files with 4702 additions and 24288 deletions

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 && yarn install",
"postCreateCommand": "poetry config virtualenvs.in-project true && poetry install -E all && poetry run pre-commit install && npm install -g pnpm && pnpm install",
"customizations": {
"vscode": {
"settings": {

61
.github/ISSUE_TEMPLATE/issue.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: 问题报告
description: 我遇到了问题
body:
- type: markdown
id: environment
attributes:
value: |
## 环境
- type: input
id: nonebot_bison_version
attributes:
label: nonebot-bison 版本
description: 请填写 nonebot-bison 的版本
- type: input
id: nonebot_version
attributes:
label: nonebot 版本
description: 请填写 nonebot 的版本
- type: dropdown
id: installation_method
attributes:
label: 安装方式
description: 请选择安装方式
options:
- 通过 nb-cli 安装
- 使用 poetry/pdm 等现代包管理器安装
- 通过 pip install 安装
- 克隆或下载项目直接使用
- type: input
id: operating_system
attributes:
label: 操作系统
description: 请填写操作系统
- type: textarea
id: issue_description
attributes:
label: 问题
description: 请在这里描述你遇到的问题
- type: textarea
id: logs
attributes:
label: 日志
description: 请在这里粘贴你的日志
render: shell
- type: checkboxes
id: confirmations
attributes:
label: 确认
options:
- label: 我搜索过了 issue但是并没有发现过与我类似的问题
required: true
- label: 我确认在日志中去掉了敏感信息
required: true

View File

@ -1,31 +0,0 @@
---
name: 问题报告
about: 我遇到了问题
title: ""
labels: ""
assignees: ""
---
## 环境
- nonebot-bison 版本:
- nonebot 版本:
- 安装方式:(以下方式的一种或者其他方式)
1. 通过 nb-cli 安装
2. 使用 poetry/pdm 等现代包管理器安装
3. 通过 pip install 安装
4. 克隆或下载项目直接使用
- 操作系统:
## 问题
请在这里描述你遇到的问题
## 日志
```
请在这里粘贴你的日志
```
- [ ] 我搜索过了 issue但是并没有发现过与我类似的问题
- [ ] 我确认在日志中去掉了敏感信息

View File

@ -10,5 +10,5 @@ runs:
- name: Build Frontend
shell: bash
run: |
yarn install
yarn docs:build
pnpm install
pnpm docs:build

View File

@ -11,5 +11,5 @@ runs:
shell: bash
working-directory: ./admin-frontend
run: |
yarn install
yarn build
pnpm install
pnpm build

View File

@ -8,11 +8,16 @@ runs:
with:
node-version: "20"
- id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- 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)"
shell: bash
- uses: actions/cache@v4
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
path: ${{ steps.pnpm-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}

View File

@ -1,5 +1,20 @@
# Change Log
## v0.9.5
### 新功能
- :sparkles: 更新默认UA为Windows平台 [@suyiiyii](https://github.com/suyiiyii) ([#643](https://github.com/MountainDash/nonebot-bison/pull/643))
### Bug 修复
- 🐛 修复微博更换长内容接口 [@phidiaLam](https://github.com/phidiaLam) ([#645](https://github.com/MountainDash/nonebot-bison/pull/645))
- :bug: 修复B站获取匿名Cookie逻辑 [@suyiiyii](https://github.com/suyiiyii) ([#644](https://github.com/MountainDash/nonebot-bison/pull/644))
### 文档
- 📝 小刻食堂剪彩文档 [@phidiaLam](https://github.com/phidiaLam) ([#636](https://github.com/MountainDash/nonebot-bison/pull/636))
## v0.9.4
### Bug 修复

View File

@ -10,7 +10,6 @@ import WeightConfig from './features/weightConfig/WeightManager';
import Home from './pages/Home';
import Unauthed from './pages/Unauthed';
import CookieManager from './features/cookieManager/CookieManager';
import CookieTargetManager from './features/cookieTargetManager/CookieTargetManager';
function App() {
const dispatch = useAppDispatch();
@ -53,8 +52,8 @@ function App() {
element: <CookieManager />,
},
{
path: 'cookie/:cookieId',
element: <CookieTargetManager />,
path: 'cookie/:siteName',
element: <CookieManager />,
},
],
},

View File

@ -0,0 +1,78 @@
import React, { useState } from 'react';
import { Form, Input, Modal } from '@arco-design/web-react';
import { useNewCookieMutation } from './cookieConfigSlice';
import { useAppDispatch } from '../../app/hooks';
import validateCookie from './cookieValidateReq';
interface CookieAddModalProps {
visible: boolean;
setVisible: (arg0: boolean) => void;
siteName: string;
}
function CookieAddModal({ visible, setVisible, siteName }: CookieAddModalProps) {
const FormItem = Form.Item;
const [content, setContent] = useState<string>('');
const [confirmLoading, setConfirmLoading] = useState(false);
const [newCookie] = useNewCookieMutation();
const dispatch = useAppDispatch();
const onSubmit = () => {
const postPromise: ReturnType<typeof newCookie> = newCookie({ siteName, content });
setConfirmLoading(true);
postPromise.then(() => {
setConfirmLoading(false);
setVisible(false);
setContent('');
});
};
return (
<Modal
title="添加 Cookie"
visible={visible}
onCancel={() => setVisible(false)}
confirmLoading={confirmLoading}
onOk={onSubmit}
style={{ maxWidth: '90vw' }}
>
<Form autoComplete="off">
<FormItem label="站点" required>
<Input placeholder="Please enter site name" value={siteName} disabled />
</FormItem>
<FormItem
label="Cookie"
required
field="content"
hasFeedback
rules={[
{
validator: (value, callback) => new Promise<void>((resolve) => {
dispatch(validateCookie(siteName, value))
.then((res) => {
if (res) {
callback();
} else {
callback('Cookie 格式错误');
}
resolve();
});
}),
},
]}
>
<Input.TextArea
placeholder="请输入 Cookie"
value={content}
onChange={setContent}
/>
</FormItem>
</Form>
</Modal>
);
}
export default CookieAddModal;

View File

@ -0,0 +1,128 @@
import React, { useState } from 'react';
import {
Button, Empty, Form, Input, Modal, Space, Table,
} from '@arco-design/web-react';
import { useDeleteCookieTargetMutation, useGetCookieTargetsQuery } from './cookieConfigSlice';
import { Cookie, CookieTarget } from '../../utils/type';
import CookieTargetModal from '../cookieTargetManager/CookieTargetModal';
interface CookieEditModalProps {
visible: boolean;
setVisible: (arg0: boolean) => void;
cookie: Cookie | null
}
function CookieEditModal({ visible, setVisible, cookie }: CookieEditModalProps) {
if (!cookie) {
return <Empty />;
}
const FormItem = Form.Item;
// const [confirmLoading, setConfirmLoading] = useState(false);
const [deleteCookieTarget] = useDeleteCookieTargetMutation();
// 获取 Cookie Target
const { data: cookieTargets } = useGetCookieTargetsQuery({ cookieId: cookie.id });
// 添加 Cookie Target
const [showAddCookieTargetModal, setShowAddCookieTargetModal] = useState(false);
const handleAddCookieTarget = () => () => {
setShowAddCookieTargetModal(true);
};
// 删除 Cookie Target
const handleDelete = (record: CookieTarget) => () => {
deleteCookieTarget({
cookieId: record.cookie_id,
target: record.target.target,
platformName: record.target.platform_name,
});
};
const columns = [
{
title: '平台名称',
dataIndex: 'target.platform_name',
},
{
title: '订阅名称',
dataIndex: 'target.target_name',
},
{
title: 'Cookie ID',
dataIndex: 'cookie_id',
},
{
title: '操作',
dataIndex: 'op',
render: (_: null, record: CookieTarget) => (
<Space size="small">
<Button type="text" status="danger" onClick={handleDelete(record)}></Button>
</Space>
),
},
];
return (
<>
<Modal
title="编辑 Cookie"
visible={visible}
onCancel={() => setVisible(false)}
// confirmLoading={confirmLoading}
onOk={() => setVisible(false)}
style={{ maxWidth: '90vw', minWidth: '50vw' }}
>
<Form autoComplete="off">
<FormItem label="Cookie ID">
<Input disabled value={cookie.id.toString()} />
</FormItem>
<FormItem label="Cookie 名称">
<Input value={cookie.cookie_name} disabled />
</FormItem>
<FormItem label="所属站点">
<Input value={cookie.site_name} disabled />
</FormItem>
<FormItem label="内容">
<Input.TextArea
value={cookie.content}
disabled
/>
</FormItem>
<FormItem label="标签">
<Input.TextArea
value={JSON.stringify(cookie.tags)}
disabled
/>
</FormItem>
<FormItem label="最后使用时间">
<Input value={cookie.last_usage.toString()} disabled />
</FormItem>
<FormItem label="状态">
<Input value={cookie.status} disabled />
</FormItem>
<FormItem label="冷却时间(毫秒)">
<Input value={cookie.cd_milliseconds.toString()} disabled />
</FormItem>
</Form>
<Button type="primary" onClick={handleAddCookieTarget()}> Cookie</Button>
<Table
columns={columns}
data={cookieTargets}
rowKey={(record: CookieTarget) => `${record.target.platform_name}-${record.target.target}`}
scroll={{ x: true }}
/>
</Modal>
<CookieTargetModal
cookie={cookie}
visible={showAddCookieTargetModal}
setVisible={setShowAddCookieTargetModal}
/>
</>
);
}
export default CookieEditModal;

View File

@ -1,120 +1,111 @@
import React, { useState } from 'react';
import React from 'react';
import {
Button,
Card, Descriptions, Grid, List, Popconfirm, Popover, Typography,
Table, TableColumnProps, Typography, Space, Popconfirm,
} from '@arco-design/web-react';
import { Link } from 'react-router-dom';
import { IconDelete, IconEdit } from '@arco-design/web-react/icon';
import { selectSiteConf } from '../globalConf/globalConfSlice';
import { useAppSelector } from '../../app/hooks';
import { Cookie, SiteConfig } from '../../utils/type';
import { useParams } from 'react-router-dom';
import { useGetCookiesQuery, useDeleteCookieMutation } from './cookieConfigSlice';
import CookieModal from './CookieModal';
import './CookieManager.css';
interface CookieSite {
site: SiteConfig;
cookies: Cookie[];
}
import { Cookie } from '../../utils/type';
import CookieAddModal from './CookieAddModal';
import CookieEditModal from './CookieEditModal';
export default function CookieManager() {
const siteConf = useAppSelector(selectSiteConf);
const { siteName } = useParams();
const { data: cookieDict } = useGetCookiesQuery();
const cookiesList = cookieDict ? Object.values(cookieDict) : [];
const cookieSite = Object.values(siteConf).filter((site) => site.enable_cookie);
const cookieSiteList: CookieSite[] = cookieSite.map((site) => ({
site,
cookies: cookiesList.filter((cookie) => cookie.site_name === site.name),
}));
const [showModal, setShowModal] = useState(false);
const [siteName, setSiteName] = useState('');
const [deleteCookie] = useDeleteCookieMutation();
const handleAddCookie = (newSiteName: string) => () => {
console.log(newSiteName);
setSiteName(newSiteName);
setShowModal(true);
// 添加cookie
const [showAddModal, setShowAddModal] = React.useState(false);
const handleAddCookie = () => () => {
setShowAddModal(true);
};
// 删除cookie
const [deleteCookie] = useDeleteCookieMutation();
const handleDelCookie = (cookieId: string) => () => {
console.log(cookieId);
deleteCookie({
cookieId,
});
};
// 编辑cookie
const [showEditModal, setShowEditModal] = React.useState(false);
const [editCookie, setEditCookie] = React.useState<Cookie | null>(null);
const handleEditCookie = (cookie: Cookie) => () => {
setEditCookie(cookie);
setShowEditModal(true);
};
let data = [];
if (siteName) {
data = cookiesList.filter((tSite) => tSite.site_name === siteName);
}
const columns: TableColumnProps[] = [
{
title: 'ID',
dataIndex: 'id',
},
{
title: 'Cookie 名称',
dataIndex: 'cookie_name',
},
{
title: '所属站点',
dataIndex: 'site_name',
},
{
title: '最后使用时间',
dataIndex: 'last_usage',
},
{
title: '状态',
dataIndex: 'status',
},
{
title: 'CD',
dataIndex: 'cd_milliseconds',
}, {
title: '操作',
dataIndex: 'op',
render: (_: null, record: Cookie) => (
<Space size="small">
<Popconfirm
title={`确定删除 Cookie ${record.cookie_name} `}
onOk={handleDelCookie(record.id.toString())}
>
<span className="list-actions-icon">
{/* <IconDelete /> */}
<Button type="text" status="danger"></Button>
</span>
</Popconfirm>
<Button type="text" onClick={handleEditCookie(record)}></Button>
</Space>
),
},
];
return (
<>
<Typography.Title heading={4} style={{ margin: '15px' }}>Cookie </Typography.Title>
<div>
<Grid.Row gutter={20}>
{cookieSiteList && cookieSiteList.map(({ cookies, site }) => (
<Grid.Col xs={24} sm={12} md={8} lg={6} key={site.name} style={{ margin: '1em 0' }}>
<Card
title={site.name}
extra={(
<Button
type="primary"
onClick={handleAddCookie(site.name)}
>
</Button>
)}
>
<Typography.Title heading={4} style={{ margin: '15px' }}>Cookie </Typography.Title>
{cookies.map((cookie) => (
<List
bordered={false}
>
<Button
style={{ width: '90px', margin: '20px 10px' }}
type="primary"
onClick={handleAddCookie()}
>
</Button>
</div>
<List.Item
key={cookie.id}
style={{ padding: '20px 0', borderBottom: '1px solid var(--color-fill-3)' }}
>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Popover
key={cookie.id}
title={cookie.friendly_name}
content={(
<Descriptions
column={1}
title="Cookie 详情"
data={Object.entries(cookie).map((entry) => ({
label: entry[0].toString(),
value: typeof (entry[1]) === 'object' ? JSON.stringify(entry[1]) : entry[1].toString(),
}))}
/>
)}
>
{cookie.friendly_name}
</Popover>
<div style={{ display: 'flex' }}>
<Link to={`/home/cookie/${cookie.id}`}>
<span className="list-actions-icon">
<IconEdit />
</span>
</Link>
<Popconfirm
title={`确定删除 Cookie ${cookie.friendly_name} `}
onOk={handleDelCookie(cookie.id.toString())}
>
<span className="list-actions-icon">
<IconDelete />
</span>
</Popconfirm>
</div>
</div>
</List.Item>
</List>
))}
</Card>
</Grid.Col>
))}
</Grid.Row>
<CookieModal visible={showModal} setVisible={setShowModal} siteName={siteName} />
<Table columns={columns} data={data} />
<CookieAddModal visible={showAddModal} setVisible={setShowAddModal} siteName={siteName || ''} />
<CookieEditModal visible={showEditModal} setVisible={setShowEditModal} cookie={editCookie} />
</>
);
}

View File

@ -1,54 +0,0 @@
import React, { useState } from 'react';
import { Form, Input, Modal } from '@arco-design/web-react';
import { useNewCookieMutation } from './cookieConfigSlice';
interface CookieModalProps {
visible: boolean;
setVisible: (arg0: boolean) => void;
siteName: string;
}
function CookieModal({ visible, setVisible, siteName }: CookieModalProps) {
const FormItem = Form.Item;
const [content, setContent] = useState<string>('');
const [confirmLoading, setConfirmLoading] = useState(false);
const [newCoookie] = useNewCookieMutation();
const onSubmit = () => {
const postPromise: ReturnType<typeof newCoookie> = newCoookie({ siteName, content });
setConfirmLoading(true);
postPromise.then(() => {
setConfirmLoading(false);
setVisible(false);
setContent('');
});
};
return (
<Modal
title="添加 Cookie"
visible={visible}
onCancel={() => setVisible(false)}
confirmLoading={confirmLoading}
onOk={onSubmit}
style={{ maxWidth: '90vw' }}
>
<Form autoComplete="off">
<FormItem label="Site Name" required>
<Input placeholder="Please enter site name" value={siteName} disabled />
</FormItem>
<FormItem label="Content" required>
<Input.TextArea
placeholder="Please enter content"
value={content}
onChange={setContent}
/>
</FormItem>
</Form>
</Modal>
);
}
export default CookieModal;

View File

@ -40,8 +40,8 @@ export const cookieTargetApi = createApi({
baseQuery: baseQueryWithAuth,
tagTypes: ['CookieTarget'],
endpoints: (builder) => ({
getCookieTargets: builder.query<CookieTarget[], {cookieId: number }>({
query: (cookieId) => `/cookie_target?cookie_id=${cookieId}`,
getCookieTargets: builder.query<CookieTarget[], { cookieId: number }>({
query: ({ cookieId }) => `/cookie_target?cookie_id=${cookieId}`,
providesTags: ['CookieTarget'],
}),
newCookieTarget: builder.mutation<StatusResp, NewCookieTargetParam>({

View File

@ -0,0 +1,20 @@
import { AppThunk } from '../../app/store';
import { baseUrl } from '../../utils/urls';
// eslint-disable-next-line
export const validCookie =
(siteName: string, content: string): AppThunk<Promise<string>> => async (_, getState) => {
const url = `${baseUrl}cookie/validate?site_name=${siteName}&content=${content}`;
const state = getState();
const authToken = state.auth.token;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${authToken}`,
},
method: 'POST',
});
const resObj = await res.json();
return resObj.ok;
};
export default validCookie;

View File

@ -1,74 +0,0 @@
import React, { useState } from 'react';
import { useParams } from 'react-router-dom';
import {
Button, Space, Table, Typography,
} from '@arco-design/web-react';
import { useDeleteCookieTargetMutation, useGetCookieTargetsQuery } from '../cookieManager/cookieConfigSlice';
import { CookieTarget } from '../../utils/type';
import CookieTargetModal from './CookieTargetModal';
export default function () {
const { cookieId } = useParams();
const { data: cookieTargets } = useGetCookieTargetsQuery(cookieId);
const [showModal, setShowModal] = useState(false);
const [deleteCookieTarget] = useDeleteCookieTargetMutation();
const handleAdd = () => {
console.log('before', showModal);
setShowModal(true);
console.log('after', showModal);
};
const handleDelete = (record: CookieTarget) => () => {
deleteCookieTarget({
cookieId: record.cookieId,
target: record.target.target,
platformName: record.target.platform_name,
});
};
const columns = [
{
title: '平台名称',
dataIndex: 'target.platform_name',
},
{
title: '订阅名称',
dataIndex: 'target.target_name',
},
{
title: 'Cookie ID',
dataIndex: 'cookie_id',
},
{
title: '操作',
dataIndex: 'op',
render: (_: null, record: CookieTarget) => (
<Space size="small">
<Button type="text" status="danger" onClick={handleDelete(record)}></Button>
</Space>
),
},
];
return (
<>
<span>
<Typography.Title heading={3}>{`Cookie ${cookieId}`}</Typography.Title>
</span>
<Button style={{ width: '90px', margin: '20px 10px' }} type="primary" onClick={handleAdd}></Button>
<Table
columns={columns}
data={cookieTargets}
rowKey={(record: CookieTarget) => `${record.target.platform_name}-${record.target.target}`}
scroll={{ x: true }}
/>
<CookieTargetModal
key={cookieId}
visible={showModal}
setVisible={setShowModal}
cookieId={cookieId}
/>
</>
);
}

View File

@ -1,18 +1,40 @@
import React
from 'react';
import { Modal, Select } from '@arco-design/web-react';
import { SubscribeConfig, SubscribeGroupDetail } from '../../utils/type';
import {
Empty, Form, Modal, Select,
} from '@arco-design/web-react';
import { Cookie, SubscribeConfig, SubscribeGroupDetail } from '../../utils/type';
import { useNewCookieTargetMutation } from '../cookieManager/cookieConfigSlice';
import { useGetSubsQuery } from '../subsribeConfigManager/subscribeConfigSlice';
import { useAppSelector } from '../../app/hooks';
import { selectPlatformConf } from '../globalConf/globalConfSlice';
interface SubscribeModalProp {
cookieId: number;
cookie:Cookie| null
visible: boolean;
setVisible: (arg0: boolean) => void;
}
export default function ({ cookieId, visible, setVisible }: SubscribeModalProp) {
export default function CookieTargetModal({
cookie, visible, setVisible,
}: SubscribeModalProp) {
if (!cookie) {
return <Empty />;
}
const [newCookieTarget] = useNewCookieTargetMutation();
const FormItem = Form.Item;
// 筛选出当前Cookie支持的平台
const platformConf = useAppSelector(selectPlatformConf);
const platformThatSiteSupport = Object.values(platformConf).reduce((p, c) => {
if (c.siteName in p) {
p[c.siteName].push(c.platformName);
} else {
p[c.siteName] = [c.platformName];
}
return p;
}, {} as Record<string, string[]>);
const supportedPlatform = platformThatSiteSupport[cookie.site_name];
const { data: subs } = useGetSubsQuery();
const pureSubs:SubscribeConfig[] = subs ? Object.values(subs)
@ -20,18 +42,21 @@ export default function ({ cookieId, visible, setVisible }: SubscribeModalProp)
pv:Array<SubscribeConfig>,
cv:SubscribeGroupDetail,
) => pv.concat(cv.subscribes), []) : [];
const filteredSubs = pureSubs.filter((sub) => supportedPlatform.includes(sub.platformName));
const [index, setIndex] = React.useState(-1);
const handleSubmit = (idx:number) => {
const postPromise: ReturnType<typeof newCookieTarget> = newCookieTarget({
cookieId,
platformName: pureSubs[idx].platformName,
target: pureSubs[idx].target,
cookieId: cookie.id,
platformName: filteredSubs[idx].platformName,
target: filteredSubs[idx].target,
});
postPromise.then(() => {
setVisible(false);
});
};
const { Option } = Select;
return (
<Modal
title="关联 Cookie"
@ -39,22 +64,46 @@ export default function ({ cookieId, visible, setVisible }: SubscribeModalProp)
onCancel={() => setVisible(false)}
onOk={() => handleSubmit(index)}
>
<Select
placeholder="选择要关联的 target"
style={{ width: '100%' }}
onChange={setIndex}
>
{pureSubs.length
&& pureSubs.map((sub, idx) => (
<Form>
<FormItem label="平台">
<Select
placeholder="选择要关联的平台"
style={{ width: '100%' }}
onChange={setIndex}
>
{supportedPlatform.length
&& supportedPlatform.map((sub, idx) => (
<Option
key={JSON.stringify(sub)}
value={idx}
>
{JSON.stringify(sub)}
{sub}
</Option>
))}
</Select>
</Select>
</FormItem>
<FormItem label="订阅目标" required>
<Select
placeholder="选择要关联的订阅目标"
style={{ width: '100%' }}
onChange={setIndex}
>
{filteredSubs.length
&& filteredSubs.map((sub, idx) => (
<Option
key={JSON.stringify(sub)}
value={idx}
>
{sub.targetName}
</Option>
))}
</Select>
</FormItem>
</Form>
</Modal>
);
}

View File

@ -9,6 +9,7 @@ import {
} from 'react-router-dom';
import { useAppSelector } from '../app/hooks';
import { selectIsLogin } from '../features/auth/authSlice';
import { selectSiteConf } from '../features/globalConf/globalConfSlice';
export default function Home() {
const location = useLocation();
@ -38,7 +39,7 @@ export default function Home() {
} else if (path.startsWith('/home/groups/')) {
currentKey = 'subs';
} else if (path.startsWith('/home/cookie/')) {
currentKey = 'cookie';
currentKey = path.substring(6);
}
const [selectedTab, changeSelectTab] = useState(currentKey);
@ -51,6 +52,8 @@ export default function Home() {
navigate('/home/weight');
} else if (tab === 'cookie') {
navigate('/home/cookie');
} else if (tab.startsWith('cookie/')) {
navigate(`/home/${tab}`);
}
};
@ -103,6 +106,10 @@ export default function Home() {
</Breadcrumb>
);
}
const MenuItem = Menu.Item;
const { SubMenu } = Menu;
const siteConf = useAppSelector(selectSiteConf);
return (
<Layout className="layout-collapse-demo">
<Layout.Header>
@ -125,15 +132,25 @@ export default function Home() {
<IconRobot />
</Menu.Item>
<Menu.Item key="cookie">
<IconIdcard />
Cookie
</Menu.Item>
<SubMenu
key="cookie"
title={(
<>
<IconIdcard />
Cookie
</>
)}
>
{Object.values(siteConf).filter((site) => site.enable_cookie).map((site) => (
<MenuItem key={`cookie/${site.name}`}>
{site.name}
</MenuItem>
))}
</SubMenu>
<Menu.Item key="weight">
<IconDashboard />
</Menu.Item>
</Menu>
</Layout.Sider>
<Layout.Content style={{ padding: '0 1em' }}>

View File

@ -28,12 +28,13 @@ export interface PlatformConfig {
categories: CategoryConfig;
enabledTag: boolean;
platformName: string;
siteName: string;
hasTarget: boolean;
}
export interface SiteConfig {
name: string
enable_cookie: string
name: string;
enable_cookie: string;
}
export interface SubscribeConfig {
@ -90,7 +91,8 @@ export interface Target {
export interface Cookie {
id: number;
site_name: string;
friendly_name: string;
content: string;
cookie_name: string;
last_usage: Date;
status: string;
cd_milliseconds: number;
@ -101,16 +103,16 @@ export interface Cookie {
export interface CookieTarget {
target: Target;
cookieId: number;
cookie_id: number;
}
export interface NewCookieParam {
siteName: string
content: string
siteName: string;
content: string;
}
export interface DelCookieParam {
cookieId: string
cookieId: string;
}
export interface NewCookieTargetParam {

View File

@ -22,7 +22,7 @@ BISON_SKIP_BROWSER_CHECK=false
BISON_USE_PIC_MERGE=0
BISON_RESEND_TIMES=0
BISON_PROXY=
BISON_UA=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36
BISON_UA=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0
BISON_SHOW_NETWORK_WARNING=true
BISON_PLATFORM_THEME='{}'

View File

@ -1,7 +1,8 @@
FROM node:20.17.0 as frontend
ADD . /app
WORKDIR /app/admin-frontend
RUN yarn && yarn build
RUN npm install -g pnpm
RUN pnpm install && pnpm build
FROM python:3.11
RUN python3 -m pip install poetry && poetry config virtualenvs.create false

View File

@ -23,11 +23,29 @@ export default navbar([
link: "",
activeMatch: "^/usage/?$",
},
{
text: "Cookie 使用",
icon: "cookie",
link: "cookie",
},
],
},
{
text: "开发",
icon: "flask",
link: "/dev/",
prefix: "/dev/",
children: [
{
text: "基本开发",
icon: "tools",
link: "",
activeMatch: "^/dev/?$",
},
{
text: "Cookie 开发",
icon: "cookie",
link: "cookie",
},
],
},
]);

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -80,6 +80,7 @@ export default hopeTheme({
sup: true,
tabs: true,
vPre: true,
mermaid: true,
// 在启用之前安装 chart.js
// chart: true,
@ -101,9 +102,6 @@ export default hopeTheme({
// 在启用之前安装 mathjax-full
// mathjax: true,
// 在启用之前安装 mermaid
// mermaid: true,
// playground: {
// presets: ["ts", "vue"],
// },

View File

@ -70,7 +70,7 @@ highlights:
details: 一个由玩家创造的幻想世界
link: https://adsrff.web.sdo.com/web1/
- title: 小刻食堂 (即将支持)
- title: 小刻食堂
details: 实时获取鹰角发的动态
link: https://www.ceobecanteen.top

View File

@ -1,3 +1,8 @@
---
prev: /usage/install
next: /dev/cookie
---
# 基本开发须知
## 语言以及工具

157
docs/dev/cookie.md Normal file
View File

@ -0,0 +1,157 @@
---
prev: /usage/
#next: /dev/cookie
---
# Cookie 开发须知
本项目将大部分 Cookie 相关逻辑提出到了 Site 及 ClientManger 模块中,只需要继承相关类即可获得使用 Cookie 的能力。
::: tip
在开发 Cookie 功能之前,你应该对[基本开发](/dev/#基本开发)有一定的了解。
:::
## Cookie 相关的基本概念
- `nonebot_bison.config.db_model.Cookie`: 用于存储 Cookie 的实体类,包含了 Cookie 的名称、内容、状态等信息
- `nonebot_bison.config.db_model.CookieTarget`: 用于存储 Cookie 与订阅的关联关系
- `nonebot_bison.utils.site.CookieClientManager`: 添加了 Cookie 功能的 ClientManager是 Cookie 管理功能的核心,调度 Cookie 的功能就在这里实现
## 快速上手
例如,现在有一个这样子的 Site 类:
```python
class WeiboSite(Site):
name = "weibo.com"
schedule_type = "interval"
schedule_setting = {"seconds": 3}
```
简而言之,要让站点获得 Cookie 能力,只需要:
为 Site 类添加一个`client_mgr`字段,值为`CookieClientManager.from_name(name)`,其中`name`为站点名称,这是默认的 Cookie 管理器。
```python {5}
class WeiboSite(Site):
name = "weibo.com"
schedule_type = "interval"
schedule_setting = {"seconds": 3}
client_mgr = CookieClientManager.from_name(name)
```
至此,站点就可以使用 Cookie 了!
## 更好的体验
为了给用户提供更好的体验,还可以创建自己的 `ClientManager`:继承 `CookieClientManager` 并重写`validate_cookie``get_target_name`方法。
- `async def validate_cookie(cls, content: str) -> bool`该方法将会在 Cookie 添加时被调用,可以在这里验证 Cookie 的有效性
- `async def get_cookie_name(cls, content: str) -> str`该方法将会在验证 Cookie 成功后被调用,可以在这里设置 Cookie 的名字并展示给用户
## 自定义 Cookie 调度策略
当默认的 Cookie 调度逻辑无法满足需求时,可以重写`CookieClientManager``_choose_cookie`方法。
目前整体的调度逻辑是:
```mermaid
sequenceDiagram
participant Scheduler
participant Platform
participant CookieClientManager
participant DB
participant Internet
Scheduler->>Platform: exec_fetch
Platform->>Platform: do_fetch_new_post(SubUnit)
Platform->>Platform: get_sub_list(Target)
Platform->>CookieClientManager: get_client(Target)
CookieClientManager->>CookieClientManager: _choose_cookie(Target)
CookieClientManager->>DB: get_cookies()
CookieClientManager->>CookieClientManager: _assemble_client(Target, cookie)
CookieClientManager->>Platform: client
Platform->>Internet: client.get(Target)
Internet->>Platform: response
Platform->>CookieClientManager: _response_hook()
CookieClientManager->>DB: update_cookie()
```
目前 CookieClientManager 具有以下方法
- `refresh_anonymous_cookie(cls)` 移除已有的匿名 cookie添加一个新的匿名 cookie应该在 CCM 初始化时调用
- `add_user_cookie(cls, content: str)` 添加用户 cookie在这里对 Cookie 进行检查并获取 cookie_name写入数据库
- `_generate_hook(self, cookie: Cookie) -> Callable` hook 函数生成器,用于回写请求状态到数据库
- `_choose_cookie(self, target: Target) -> Cookie` 选择 cookie 的具体算法
- `add_user_cookie(cls, content: str, cookie_name: str | None = None) -> Cookie` 对外的接口,添加用户 cookie内部会调用 Site 的方法进行检查
- `get_client(self, target: Target | None) -> AsyncClient` 对外的接口,获取 client根据 target 选择 cookie
- `_assemble_client(self, client, cookie) -> AsyncClient` 组装 client可以自定义 cookie 对象的 content 装配到 client 中的方式
::: details 大致流程
1. `Platfrom` 调用 `CookieClientManager.get_client` 方法,传入 `Target` 对象
2. `CookieClientManager` 根据 `Target` 选择一个 `Cookie` 对象
3. `CookieClientManager` 调用 `CookieClientManager._assemble_client` 方法,将 Cookie 装配到 `Client`
4. `Platform` 使用 `Client` 进行请求
:::
简单来说:
- 如果需要修改 Cookie 的默认参数,可以重写`add_user_cookie`方法,这里设置需要的字段
- 如果需要修改选择 Cookie 的逻辑,可以重写`_choose_cookie`方法,使用自己的算法选择合适的 Cookie 并返回
- 如果需要自定义 Cookie 的格式,可以重写`valid_cookie`方法,自定义验证 Cookie 的逻辑,并重写`_assemble_client`方法,自定义将 Cookie 装配到 Client 中的逻辑
- 如果要在请求结束后做一些操作(例如保存此次请求的结果/状态),可以重写`_response_hook`方法,自定义请求结束后的行为
- 如果需要跳过一次请求,可以在 `get_client` 方法中抛出 `SkipRequestException` 异常,调度器会捕获该异常并跳过此次请求
## 实名 Cookie 和匿名 Cookie
部分站点所有接口都需要携带 Cookie对于匿名用户未登录也会发放一个临时 Cookie本项目称为匿名 Cookie。
在此基础上,我们添加了用户上传 Cookie 的功能,这种 Cookie 本项目称为实名 Cookie。
匿名 Cookie 和实名 Cookie 在同一个框架下统一调度,实名 Cookie 优先级高于匿名 Cookie。为了调度Cookie 对象有以下字段:
```python
# 最后使用的时刻
last_usage: Mapped[datetime.datetime] = mapped_column(DateTime, default=datetime.datetime(1970, 1, 1))
# Cookie 当前的状态
status: Mapped[str] = mapped_column(String(20), default="")
# 使用一次之后,需要的冷却时间
cd_milliseconds: Mapped[int] = mapped_column(default=0)
# 是否是通用 Cookie对所有 Target 都有效)
is_universal: Mapped[bool] = mapped_column(default=False)
# 是否是匿名 Cookie
is_anonymous: Mapped[bool] = mapped_column(default=False)
# 标签,扩展用
tags: Mapped[dict[str, Any]] = mapped_column(JSON().with_variant(JSONB, "postgresql"), default={})
```
其中:
- **is_universal**:用于标记 Cookie 是否为通用 Cookie即对所有 Target 都有效。可以理解为是一种特殊的 target添加 Cookie 和获取 Cookie 时通过传入参数进行设置。
- **is_anonymous**:用于标记 Cookie 是否为匿名 Cookie目前的定义是可以由程序自动生成的适用于所有 Target 的 Cookie。目前的逻辑是 bison 启动时,生成一个新的匿名 Cookie 并替换掉原有的匿名 Cookie。
- **无 Target 平台的 Cookie 处理方式**
对于不存在 Target 的平台,如小刻食堂,可以重写 add_user_cookie 方法,为用户 Cookie 设置 is_universal 字段。这样,在获取 Client 时,由于传入的 Target 为空,就只会选择 is_universal 的 cookie。实现了无 Target 平台的用户 Cookie 调度。
## 默认的调度策略
默认的调度策略在 CookieClientManager 的 `_choose_cookie` 方法中实现:
```python
async def _choose_cookie(self, target: Target | None) -> Cookie:
"""选择 cookie 的具体算法"""
cookies = await config.get_cookie(self._site_name, target)
cookies = (cookie for cookie in cookies if cookie.last_usage + cookie.cd < datetime.now())
cookie = min(cookies, key=lambda x: x.last_usage)
return cookie
```
简而言之,会选择最近使用时间最早的 Cookie且不在冷却时间内的 Cookie。
在默认情况下,匿名 Cookie 的冷却时间为 0实名 Cookie 的冷却时间为 10 秒。也就是说,调度时,如果没有可用的实名 Cookie则会选择匿名 Cookie。

View File

@ -1,6 +1,6 @@
---
prev: /usage/install
next: /usage/easy-use
next: /usage/cookie
---
# 全方位了解 Bison 的自行车
@ -272,6 +272,21 @@ Bison 在处理每条推送时,会按照以下规则顺序检查推送中的 T
3. **需订阅 Tag** 列表为空
- **发送**该推送到群中,检查结束
#### Cookie 功能
Bison 支持携带 Cookie 进行请求。
目前支持的平台有:
- `rss`: RSS
- `weibo`: 新浪微博
::: warning 使用须知
Cookie 全局生效,这意味着,通过你的 Cookie 获取到的内容,可能会被发给其他用户。
:::
管理员可以通过**命令**或**管理后台**给 Bison 设置 Cookie。
<script setup lang="ts">
import { ref, computed } from 'vue';

113
docs/usage/cookie.md Normal file
View File

@ -0,0 +1,113 @@
---
prev: /usage/
next: /usage/install
---
# :cookie: Bison 的自行车电助力装置
Bison 支持 Cookie 啦,你可以将 Cookie 关联到订阅以获得更好的体验。
但是,盲目使用 Cookie 功能并不能解决问题,反而可能为你的账号带来风险。请阅读完本文档后再决定是否使用 Cookie 功能。
::: warning 免责声明
Bison 具有良好的风控应对机制,我们会尽力保护你的账户,但是无法保证绝对的安全。
nonebot-bison 开发者及 MountainDash 社区不对因使用 Cookie 导致的任何问题负责。
:::
## :monocle_face: 什么时候需要 Cookie
首先,请确认 Cookie 的使用场景,并了解注意事项。
在绝大多数情况下Bison 不需要 Cookie 即可正常工作。但是部分平台只能够获取有限的内容此时Cookie 就可以帮助 Bison 获取更多的内容。
例如,微博平台可以设置微博为“仅粉丝可见”,正常情况下 Bison 无法获取到这些内容。如果你的账号是该博主的粉丝,那么你可以将你的 Cookie 关联到 Bison这样 Bison 就可以获取到这些受限内容。
::: warning 使用须知
Cookie 全局生效,这意味着,通过你的 Cookie 获取到的内容,可能会被共享给其他用户。
当然Bison 不会将你的 Cookie 透露给其他用户。但是,管理员或其他可以接触的数据库的人员可以看到**所有 Cookie**的内容。所以,在上传 Cookie 之前,请确保管理人员可信。
:::
## :wheelchair: 我该怎么使用 Cookie
首先,需要明确的是,因为 Cookie 具有隐私性,所有与 Cookie 相关的操作,仅支持**管理员**通过**私聊**或者通过**WebUI**进行管理。
目前,使用 Cookie 主要有两个步骤:
- **添加 Cookie**: 将 Cookie 发给 Bison
- **关联 Cookie**: 告诉 Bison你希望在什么时候使用这个 Cookie
## :nerd_face: 如何获取 Cookie
对于大部分平台Bison 支持 JSON 格式的 Cookie你可以通过浏览器的开发者工具获取。
- RSS: 对于各种 RSS 订阅,你需要自行准备需要的 Cookie以 JSON 格式添加即可
- 微博Bison 兼容 RSSHub 的 Cookie以下方法引用自[RSSHub 的文档](https://docs.rsshub.app/zh/deploy/config#%E5%BE%AE%E5%8D%9A)
> 1. 打开并登录 https://m.weibo.cn确保打开页面为手机版如果强制跳转电脑端可尝试使用可更改 UserAgent 的浏览器插件)
> 2. 按下 F12 打开控制台,切换至 Network网络面板
> 3. 在该网页切换至任意关注分组,并在面板打开最先捕获到的请求(该情形下捕获到的请求路径应包含/feed/group
> 4. 查看该请求的 Headers请求头, 找到 Cookie 字段并复制内容
- Bilibili: Bison 兼容 RSSHub 的 Cookie以下方法引用自[RSSHub 的文档](https://docs.rsshub.app/zh/deploy/config#bilibili)
> 1. 打开 https://api.vc.bilibili.com/dynamic_svr/v1/dynamic_svr/dynamic_new?uid=0&type=8
> 2. 打开控制台,切换到 Network 面板,刷新
> 3. 点击 dynamic_new 请求,找到 Cookie
> 4. 视频和专栏UP 主粉丝及关注只要求 SESSDATA 字段,动态需复制整段 Cookie
## :sparkles: 给 Bison 添加 Cookie
打开 Bison 的私聊,发送 `添加cookie` 命令Bison 会开始添加 Cookie 流程。
![add cookie](/images/add-cookie.png)
然后,依次输入平台名称和 Cookie 内容。
![add cookie 2](/images/add-cookie-2.png)
看到 Bison 的回复之后Cookie 就添加成功啦!
## :children_crossing: 关联 Cookie 到具体的订阅
接下来要关联 Cookie 到一个具体的订阅。
输入 `添加关联cookie` 命令Bison 就会列出当前所有的订阅。
我们选择一个订阅Bison 会列出所有的可以选择的 Cookie。
![add-cookie-target.png](/images/add-cookie-target.png)
再选择需要关联的 Cookie。
至此Bison 便会携带我们的 Cookie 去请求订阅目标啦!
## :stethoscope: 取消关联 Cookie
如果你想取消关联某个 Cookie可以发送 `取消关联cookie` 命令Bison 会列出所有已被关联的订阅和 Cookie。
选择需要取消关联的 CookieBison 会取消此 Cookie 对该订阅的关联。
这是 `添加关联cookie` 的逆向操作。
## :wastebasket: 删除 Cookie
如果你想删除某个 Cookie可以发送 `删除cookie` 命令Bison 会列出所有已添加的 Cookie。
选择需要删除的 CookieBison 会删除此 Cookie。
::: tip
只能删除未被关联的 Cookie。
也就是说,你需要先取消一个 Cookie 的所有关联,才能删除。
:::
这是 `添加cookie` 的逆向操作。
## :globe_with_meridians: 使用 WebUI 管理 Cookie
同样的Bison 提供了一个网页管理 Cookie 的功能,即 WebUI你可以在网页上查看、添加、删除 Cookie。
使用方法参见 [使用网页管理订阅](/usage/easy-use#使用网页管理订阅)。
## :tada: 完成!
至此,你已经掌握了使用 Cookie 的方法。
Congratulations! 🎉

View File

@ -61,7 +61,7 @@ def init_fastapi(driver: "Driver"):
def register_get_token_handler():
get_token = on_command("后台管理", rule=to_me(), priority=5, aliases={"管理后台"})
get_token = on_command("后台管理", rule=to_me(), priority=5, aliases={"管理后台"}, block=True)
@get_token.handle()
async def send_token(bot: "Bot", event: PrivateMessageEvent, state: T_State):

View File

@ -12,12 +12,13 @@ from fastapi.security.oauth2 import OAuth2PasswordBearer
from ..types import WeightConfig
from ..apis import check_sub_target
from .jwt import load_jwt, pack_jwt
from ..scheduler import scheduler_dict
from ..types import Target as T_Target
from ..utils.get_bot import get_groups
from ..platform import platform_manager
from .token_manager import token_manager
from ..config.db_config import SubscribeDupException
from ..platform import site_manager, platform_manager
from ..utils.site import CookieClientManager, is_cookie_client_manager
from ..utils.site import CookieClientManager, site_manager, is_cookie_client_manager
from ..config import NoSuchUserException, NoSuchTargetException, NoSuchSubscribeException, config
from .types import (
Cookie,
@ -66,7 +67,7 @@ async def get_global_conf() -> GlobalConf:
platformName=platform_name,
categories=platform.categories,
enabledTag=platform.enable_tag,
site_name=platform.site.name,
siteName=platform.site.name,
name=platform.name,
hasTarget=getattr(platform, "has_target"),
)
@ -211,12 +212,12 @@ async def update_weigth_config(platformName: str, target: str, weight_config: We
@router.get("/cookie", dependencies=[Depends(check_is_superuser)])
async def get_cookie(site_name: str = None, target: str = None) -> list[Cookie]:
# todo: 调用 client_mgr 来添加cookie以校验和获取cookie_name
cookies_in_db = await config.get_cookie(site_name, is_anonymous=False)
return [
Cookie(
id=cookies_in_db[i].id,
friendly_name=cookies_in_db[i].cookie_name,
content=cookies_in_db[i].content,
cookie_name=cookies_in_db[i].cookie_name,
site_name=cookies_in_db[i].site_name,
last_usage=cookies_in_db[i].last_usage,
status=cookies_in_db[i].status,
@ -231,7 +232,7 @@ async def get_cookie(site_name: str = None, target: str = None) -> list[Cookie]:
@router.post("/cookie", dependencies=[Depends(check_is_superuser)])
async def add_cookie(site_name: str, content: str) -> StatusResp:
client_mgr = cast(CookieClientManager, site_manager[site_name].client_mgr)
client_mgr = cast(CookieClientManager, scheduler_dict[site_manager[site_name]].client_mgr)
await client_mgr.add_user_cookie(content)
return StatusResp(ok=True, msg="")
@ -267,3 +268,12 @@ async def add_cookie_target(platform_name: str, target: str, cookie_id: int) ->
async def del_cookie_target(platform_name: str, target: str, cookie_id: int) -> StatusResp:
await config.delete_cookie_target(target, platform_name, cookie_id)
return StatusResp(ok=True, msg="")
@router.post("/cookie/validate", dependencies=[Depends(check_is_superuser)])
async def get_cookie_valid(site_name: str, content: str) -> StatusResp:
client_mgr = cast(CookieClientManager, scheduler_dict[site_manager[site_name]].client_mgr)
if await client_mgr.validate_cookie(content):
return StatusResp(ok=True, msg="")
else:
return StatusResp(ok=False, msg="")

View File

@ -6,7 +6,7 @@ class PlatformConfig(BaseModel):
categories: dict[int, str]
enabledTag: bool
platformName: str
site_name: str
siteName: str
hasTarget: bool
@ -75,7 +75,8 @@ class Target(BaseModel):
class Cookie(BaseModel):
id: int
site_name: str
friendly_name: str
content: str
cookie_name: str
last_usage: datetime
status: str
cd_milliseconds: int

View File

@ -285,6 +285,11 @@ class DBConfig:
res = [cookie for cookie in res if cookie.id in ids or cookie.is_universal]
return res
async def get_cookie_by_id(self, cookie_id: int) -> Cookie:
async with create_session() as sess:
cookie = await sess.scalar(select(Cookie).where(Cookie.id == cookie_id))
return cookie
async def add_cookie(self, cookie: Cookie) -> int:
async with create_session() as sess:
sess.add(cookie)
@ -363,5 +368,16 @@ class DBConfig:
res.sort(key=lambda x: (x.target.platform_name, x.cookie_id, x.target_id))
return res
async def clear_db(self):
"""清空数据库,用于单元测试清理环境"""
async with create_session() as sess:
await sess.execute(delete(User))
await sess.execute(delete(Target))
await sess.execute(delete(ScheduleTimeWeight))
await sess.execute(delete(Subscribe))
await sess.execute(delete(Cookie))
await sess.execute(delete(CookieTarget))
await sess.commit()
config = DBConfig()

View File

@ -10,6 +10,7 @@ from nonebot.compat import type_validate_python
from nonebot_plugin_datastore.db import create_session
from sqlalchemy.orm.strategy_options import selectinload
from .. import config
from .utils import NBESFVerMatchErr, row2dict
from .nbesf_model import NBESFBase, v1, v2, v3
from ..db_model import User, Cookie, Target, Subscribe, CookieTarget
@ -64,13 +65,22 @@ async def subscribes_export(selector: Callable[[Select], Select]) -> v3.SubGroup
target_payload = type_validate_python(v3.Target, cookie_target.target)
cookie_target_dict[cookie_target.cookie].append(target_payload)
cookies: list[v3.Cookie] = []
for cookie, targets in cookie_target_dict.items():
assert isinstance(cookie, Cookie)
def cookie_transform(cookie: Cookie, targets: [Target]) -> v3.Cookie:
cookie_dict = row2dict(cookie)
cookie_dict["tags"] = cookie.tags
cookie_dict["targets"] = targets
cookies.append(v3.Cookie(**cookie_dict))
return v3.Cookie(**cookie_dict)
cookies: list[v3.Cookie] = []
cookie_set = set()
for cookie, targets in cookie_target_dict.items():
assert isinstance(cookie, Cookie)
cookies.append(cookie_transform(cookie, targets))
cookie_set.add(cookie.id)
# 添加未关联的cookie
all_cookies = await config.get_cookie(is_anonymous=False)
cookies.extend([cookie_transform(c, []) for c in all_cookies if c.id not in cookie_set])
sub_group = v3.SubGroup(groups=groups, cookies=cookies)

View File

@ -1,10 +1,13 @@
from ..db_model import Model
class NBESFVerMatchErr(Exception): ...
class NBESFParseErr(Exception): ...
def row2dict(row):
def row2dict(row: Model) -> dict:
d = {}
for column in row.__table__.columns:
d[column.name] = str(getattr(row, column.name))

View File

@ -3,7 +3,6 @@ from pkgutil import iter_modules
from collections import defaultdict
from importlib import import_module
from ..utils import Site
from ..plugin_config import plugin_config
from .platform import Platform, make_no_target_group
@ -36,10 +35,3 @@ def _get_unavailable_platforms() -> dict[str, str]:
# platform => reason for not available
unavailable_paltforms: dict[str, str] = _get_unavailable_platforms()
site_manager: dict[str, type[Site]] = {}
for site in Site.registry:
if not hasattr(site, "name"):
continue
site_manager[site.name] = site

View File

@ -1,13 +1,17 @@
import json
import random
from typing_extensions import override
from datetime import datetime, timedelta
from typing import TYPE_CHECKING, TypeVar
from httpx import AsyncClient
from nonebot import logger, require
from playwright.async_api import Cookie
from nonebot_bison.types import Target
from nonebot_bison.utils import Site, ClientManager, http_client
from nonebot_bison.utils import Site, http_client
from ...utils.site import CookieClientManager
from ...config.db_model import Cookie as CookieModel
if TYPE_CHECKING:
from .platforms import Bilibili
@ -18,45 +22,48 @@ from nonebot_plugin_htmlrender import get_browser
B = TypeVar("B", bound="Bilibili")
class BilibiliClientManager(ClientManager):
_client: AsyncClient
_inited: bool = False
class BilibiliClientManager(CookieClientManager):
def __init__(self) -> None:
self._client = http_client()
_default_cookie_cd = timedelta(seconds=120)
async def _get_cookies(self) -> list[Cookie]:
browser = await get_browser()
async with await browser.new_page() as page:
await page.goto(f"https://space.bilibili.com/{random.randint(1, 1000)}/dynamic")
await page.wait_for_load_state("load")
await page.wait_for_load_state("load") # 等待基本加载完成
await page.wait_for_function('document.cookie.includes("bili_ticket")') # 期望保证 GenWebTicket 请求完成
await page.wait_for_load_state("networkidle") # 期望保证 ExClimbWuzhi 请求完成
cookies = await page.context.cookies()
return cookies
async def _reset_client_cookies(self, cookies: list[Cookie]):
def _gen_json_cookie(self, cookies: list[Cookie]):
cookie_dict = {}
for cookie in cookies:
self._client.cookies.set(
name=cookie.get("name", ""),
value=cookie.get("value", ""),
domain=cookie.get("domain", ""),
path=cookie.get("path", "/"),
)
cookie_dict[cookie.get("name", "")] = cookie.get("value", "")
return cookie_dict
@override
async def _generate_anonymous_cookie(self) -> CookieModel:
cookies = await self._get_cookies()
cookie = CookieModel(
cookie_name=f"{self._site_name} anonymous",
site_name=self._site_name,
content=json.dumps(self._gen_json_cookie(cookies)),
is_universal=True,
is_anonymous=True,
last_usage=datetime.now(),
cd_milliseconds=0,
tags="{}",
status="",
)
return cookie
@override
async def refresh_client(self):
cookies = await self._get_cookies()
await self._reset_client_cookies(cookies)
await self._refresh_anonymous_cookie()
logger.debug("刷新B站客户端的cookie")
@override
async def get_client(self, target: Target | None) -> AsyncClient:
if not self._inited:
logger.debug("初始化B站客户端")
await self.refresh_client()
self._inited = True
return self._client
@override
async def get_client_for_static(self) -> AsyncClient:
return http_client()

View File

@ -16,7 +16,7 @@ from nonebot_plugin_saa import PlatformTarget
from ..post import Post
from ..utils import Site, ProcessContext
from ..plugin_config import plugin_config
from ..types import Tag, Target, RawPost, SubUnit, Category, RegistryMeta
from ..types import Tag, Target, RawPost, SubUnit, Category
class CategoryNotSupport(Exception):
@ -29,6 +29,21 @@ class CategoryNotRecognize(Exception):
"""raise in get_category, when you don't know the category of post"""
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"):
# this is the base class
cls.registry = []
elif not kwargs.get("abstract"):
# this is the subclass
cls.registry.append(cls)
super().__init__(name, bases, namespace, **kwargs)
P = ParamSpec("P")
R = TypeVar("R")

View File

@ -10,14 +10,14 @@ from ..post import Post
from .platform import NewMessage
from ..types import Target, RawPost
from ..utils import text_similarity
from ..utils.site import CookieSite, create_cookie_client_manager
from ..utils.site import Site, CookieClientManager
class RssSite(CookieSite):
class RssSite(Site):
name = "rss"
schedule_type = "interval"
schedule_setting = {"seconds": 30}
client_mgr = create_cookie_client_manager("rss")
client_mgr = CookieClientManager.from_name(name)
class RssPost(Post):

View File

@ -3,6 +3,7 @@ import json
from typing import Any
from datetime import datetime
from urllib.parse import unquote
from typing_extensions import override
from yarl import URL
from lxml.etree import HTML
@ -11,10 +12,10 @@ from nonebot.log import logger
from bs4 import BeautifulSoup as bs
from ..post import Post
from ..utils import http_client
from .platform import NewMessage
from ..utils import http_client, text_fletten
from ..utils.site import Site, CookieClientManager
from ..types import Tag, Target, RawPost, ApiError, Category
from ..utils.site import CookieSite, create_cookie_client_manager
_HEADER = {
"accept": (
@ -36,11 +37,30 @@ _HEADER = {
}
class WeiboSite(CookieSite):
class WeiboClientManager(CookieClientManager):
_site_name = "weibo.com"
async def _get_current_user_name(self, cookies: dict) -> str:
url = "https://m.weibo.cn/setup/nick/detail"
async with http_client() as client:
r = await client.get(url, headers=_HEADER, cookies=cookies)
data = json.loads(r.text)
name = data["data"]["user"]["screen_name"]
return name
@override
async def get_cookie_name(self, content: str) -> str:
"""从cookie内容中获取cookie的友好名字添加cookie时调用持久化在数据库中"""
name = await self._get_current_user_name(json.loads(content))
return text_fletten(f"weibo: [{name[:10]}]")
class WeiboSite(Site):
name = "weibo.com"
schedule_type = "interval"
schedule_setting = {"seconds": 3}
client_mgr = create_cookie_client_manager(name)
client_mgr = WeiboClientManager
class Weibo(NewMessage):
@ -156,7 +176,7 @@ class Weibo(NewMessage):
try:
client = await self.ctx.get_client()
weibo_info = await client.get(
"https://m.weibo.cn/statuses/show",
"https://m.weibo.cn/statuses/extend",
params={"id": weibo_id},
headers=_HEADER,
)
@ -170,7 +190,7 @@ class Weibo(NewMessage):
async def _parse_weibo(self, info: dict) -> Post:
if info["isLongText"] or info["pic_num"] > 9:
info["text"] = (await self._get_long_weibo(info["mid"]))["text"]
info["text"] = (await self._get_long_weibo(info["mid"]))["longTextContent"]
parsed_text = self._get_text(info["text"])
raw_pics_list = info.get("pics", [])
pic_urls = [img["large"]["url"] for img in raw_pics_list]

View File

@ -36,7 +36,8 @@ class PlugConfig(BaseModel):
bison_resend_times: int = 0
bison_proxy: str | None = None
bison_ua: str = Field(
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko)"
" Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0",
description="默认UA",
)
bison_show_network_warning: bool = True

View File

@ -33,9 +33,6 @@ async def init_scheduler():
else:
_schedule_class_platform_dict[site].append(platform_name)
for site, target_list in _schedule_class_dict.items():
if is_cookie_client_manager(site.client_mgr):
client_mgr = cast(CookieClientManager, site.client_mgr)
await client_mgr.refresh_anonymous_cookie()
if not plugin_config.bison_use_browser and site.require_browser:
logger.warning(f"{site.name} requires browser, it will not schedule.")
continue
@ -47,6 +44,9 @@ async def init_scheduler():
)
platform_name_list = _schedule_class_platform_dict[site]
scheduler_dict[site] = Scheduler(site, schedulable_args, platform_name_list)
if is_cookie_client_manager(site.client_mgr):
client_mgr = cast(CookieClientManager, scheduler_dict[site].client_mgr)
await client_mgr.refresh_client()
config.register_add_target_hook(handle_insert_new_target)
config.register_delete_target_hook(handle_delete_target)

View File

@ -30,10 +30,12 @@ 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,
@ -151,7 +153,11 @@ async def do_dispatch_command(
no_permission_matcher = on_command(
"添加订阅", rule=configurable_to_me, aliases={"删除订阅", "群管理"}, priority=8, block=True
"添加订阅",
rule=configurable_to_me,
aliases={"删除订阅", "群管理", "管理后台", "添加cookie", "删除cookie", "关联cookie", "取消关联cookie"},
priority=8,
block=True,
)

View File

@ -1,20 +1,25 @@
from typing import cast
from json import JSONDecodeError
from nonebot.log import logger
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.params import Arg, ArgPlainText
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot.adapters import Message, MessageTemplate
from ..scheduler import scheduler_dict
from ..platform import platform_manager
from .utils import common_platform, gen_handle_cancel
from ..utils.site import CookieClientManager, is_cookie_client_manager
from .utils import common_platform, gen_handle_cancel, only_allow_private
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):
async def init_promote(state: T_State, event: MessageEvent):
await only_allow_private(event, add_cookie)
state["_prompt"] = (
"请输入想要添加 Cookie 的平台,目前支持,请输入冒号左边的名称:\n"
+ "".join(
@ -42,7 +47,6 @@ def do_add_cookie(add_cookie: type[Matcher]):
await add_cookie.finish("已中止添加cookie")
elif platform in platform_manager:
state["platform"] = platform
state["site"] = platform_manager[platform].site
else:
await add_cookie.reject("平台输入错误")
@ -52,18 +56,27 @@ def do_add_cookie(add_cookie: type[Matcher]):
@add_cookie.got("cookie", MessageTemplate("{_prompt}"), [handle_cancel])
async def got_cookie(state: T_State, cookie: Message = Arg()):
client_mgr: type[CookieClientManager] = cast(
type[CookieClientManager], platform_manager[state["platform"]].site.client_mgr
)
client_mgr = cast(CookieClientManager, scheduler_dict[platform_manager[state["platform"]].site].client_mgr)
cookie_text = cookie.extract_plain_text()
if not await client_mgr.validate_cookie(cookie_text):
await add_cookie.reject(state["site"].cookie_format_prompt)
state["cookie"] = cookie_text
await add_cookie.reject(
"无效的 Cookie请检查后重新输入详情见https://nonebot-bison.netlify.app/usage/cookie.html"
)
try:
cookie_name = await client_mgr.get_cookie_name(cookie_text)
state["cookie"] = cookie_text
state["cookie_name"] = cookie_name
except JSONDecodeError as e:
logger.error("获取 Cookie 名称失败" + str(e))
await add_cookie.reject(
"获取 Cookie 名称失败请检查后重新输入详情见https://nonebot-bison.netlify.app/usage/cookie.html"
)
@add_cookie.handle()
async def add_cookie_process(state: T_State):
client_mgr = cast(CookieClientManager, platform_manager[state["platform"]].site.client_mgr)
await client_mgr.add_user_cookie(state["cookie"])
client_mgr = cast(CookieClientManager, scheduler_dict[platform_manager[state["platform"]].site].client_mgr)
new_cookie = await client_mgr.add_user_cookie(state["cookie"], state["cookie_name"])
await add_cookie.finish(
f"已添加 Cookie: {state['cookie']} 到平台 {state['platform']}" + "\n请使用“关联cookie”为 Cookie 关联订阅"
f"已添加 Cookie: {new_cookie.cookie_name} 到平台 {state['platform']}"
+ "\n请使用“关联cookie”为 Cookie 关联订阅"
)

View File

@ -2,19 +2,21 @@ from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.params import ArgPlainText
from nonebot_plugin_saa import MessageFactory
from nonebot.adapters.onebot.v11 import MessageEvent
from nonebot.internal.adapter import MessageTemplate
from ..config import config
from ..utils import parse_text
from ..platform import platform_manager
from .utils import gen_handle_cancel, generate_sub_list_text
from .utils import gen_handle_cancel, only_allow_private, 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):
async def init_promote(state: T_State, event: MessageEvent):
await only_allow_private(event, add_cookie_target_matcher)
res = await generate_sub_list_text(
add_cookie_target_matcher, state, is_index=True, is_show_cookie=True, is_hide_no_cookie_platfrom=True
)

View File

@ -2,17 +2,19 @@ from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.params import EventPlainText
from nonebot_plugin_saa import MessageFactory
from nonebot.adapters.onebot.v11 import MessageEvent
from ..config import config
from ..utils import parse_text
from .utils import gen_handle_cancel
from .utils import gen_handle_cancel, only_allow_private
def do_del_cookie(del_cookie: type[Matcher]):
handle_cancel = gen_handle_cancel(del_cookie, "删除中止")
@del_cookie.handle()
async def send_list(state: T_State):
async def send_list(state: T_State, event: MessageEvent):
await only_allow_private(event, del_cookie)
cookies = await config.get_cookie(is_anonymous=False)
if not cookies:
await del_cookie.finish("暂无已添加的 Cookie\n请使用“添加cookie”命令添加")

View File

@ -2,17 +2,19 @@ from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.params import EventPlainText
from nonebot_plugin_saa import MessageFactory
from nonebot.adapters.onebot.v11 import MessageEvent
from ..config import config
from ..utils import parse_text
from .utils import gen_handle_cancel
from .utils import gen_handle_cancel, only_allow_private
def do_del_cookie_target(del_cookie_target: type[Matcher]):
handle_cancel = gen_handle_cancel(del_cookie_target, "取消关联中止")
@del_cookie_target.handle()
async def send_list(state: T_State):
async def send_list(state: T_State, event: MessageEvent):
await only_allow_private(event, del_cookie_target)
cookie_targets = await config.get_cookie_target()
if not cookie_targets:
await del_cookie_target.finish("暂无已关联 Cookie\n请使用“添加cookie”命令添加关联")

View File

@ -70,7 +70,7 @@ def admin_permission():
async def generate_sub_list_text(
matcher: type[Matcher],
state: T_State,
user_info: PlatformTarget = None,
user_info: PlatformTarget | None = None,
is_index=False,
is_show_cookie=False,
is_hide_no_cookie_platfrom=False,
@ -120,3 +120,12 @@ async def generate_sub_list_text(
res += f" (平台 {sub.target.platform_name} 已失效,请删除此订阅)"
return res
async def only_allow_private(
event: Event,
matcher: type[Matcher],
):
# if not issubclass(PrivateMessageEvent, event.__class__):
if event.message_type != "private":
await matcher.finish("请在私聊中使用此命令")

View File

@ -58,18 +58,3 @@ class ApiError(Exception):
class SubUnit(NamedTuple):
sub_target: Target
user_sub_infos: list[UserSubInfo]
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"):
# this is the base class
cls.registry = []
elif not kwargs.get("abstract"):
# this is the subclass
cls.registry.append(cls)
super().__init__(name, bases, namespace, **kwargs)

View File

@ -1,25 +1,25 @@
import difflib
import re
import sys
import difflib
import nonebot
from nonebot.plugin import require
from bs4 import BeautifulSoup as bs
from nonebot.log import logger, default_format
from nonebot.plugin import require
from nonebot_plugin_saa import Text, Image, MessageSegmentFactory
from .context import ProcessContext as ProcessContext
from .site import Site as Site
from ..plugin_config import plugin_config
from .image import pic_merge as pic_merge
from .http import http_client as http_client
from .image import capture_html as capture_html
from .image import is_pics_mergable as is_pics_mergable
from .image import pic_merge as pic_merge
from .image import pic_url_to_image as pic_url_to_image
from .image import text_to_image as text_to_image
from .site import ClientManager as ClientManager
from .site import DefaultClientManager as DefaultClientManager
from .site import Site as Site
from .image import text_to_image as text_to_image
from .site import anonymous_site as anonymous_site
from ..plugin_config import plugin_config
from .context import ProcessContext as ProcessContext
from .image import is_pics_mergable as is_pics_mergable
from .image import pic_url_to_image as pic_url_to_image
from .site import DefaultClientManager as DefaultClientManager
class Singleton(type):

View File

@ -1,17 +1,18 @@
import json
from typing import Literal
from json import JSONDecodeError
from typing import Literal, cast
from abc import ABC, abstractmethod
from collections.abc import Callable
from datetime import datetime, timedelta
import httpx
from httpx import AsyncClient
from nonebot.log import logger
from ..types import Target
from ..config import config
from .http import http_client
from ..config.db_model import Cookie
from ..types import Target, RegistryMeta
class ClientManager(ABC):
@ -42,40 +43,60 @@ class DefaultClientManager(ClientManager):
pass
class SkipRequestException(Exception):
"""跳过请求异常,如果需要在选择 Cookie 时跳过此次请求,可以抛出此异常"""
pass
class CookieClientManager(ClientManager):
_site_name: str
_default_cd: int = timedelta(seconds=10)
_default_cookie_cd = timedelta(seconds=15)
_site_name: str = ""
@classmethod
async def refresh_anonymous_cookie(cls):
"""移除已有的匿名cookie添加一个新的匿名cookie"""
anonymous_cookies = await config.get_cookie(cls._site_name, is_anonymous=True)
anonymous_cookie = Cookie(site_name=cls._site_name, content="{}", is_universal=True, is_anonymous=True)
for cookie in anonymous_cookies:
if not cookie.is_anonymous:
continue
await config.delete_cookie_by_id(cookie.id)
anonymous_cookie.id = cookie.id # 保持原有的id
anonymous_cookie.last_usage = datetime.now() # 使得第一次请求优先使用用户 cookie
await config.add_cookie(anonymous_cookie)
async def _generate_anonymous_cookie(self) -> Cookie:
return Cookie(
cookie_name=f"{self._site_name} anonymous",
site_name=self._site_name,
content="{}",
is_universal=True,
is_anonymous=True,
last_usage=datetime.now(),
cd_milliseconds=0,
tags="{}",
status="",
)
@classmethod
async def add_user_cookie(cls, content: str):
async def _refresh_anonymous_cookie(self):
"""更新已有的匿名cookie若不存在则添加"""
existing_anonymous_cookies = await config.get_cookie(self._site_name, is_anonymous=True)
if existing_anonymous_cookies:
for cookie in existing_anonymous_cookies:
new_anonymous_cookie = await self._generate_anonymous_cookie()
new_anonymous_cookie.id = cookie.id # 保持原有的id
await config.update_cookie(new_anonymous_cookie)
else:
new_anonymous_cookie = await self._generate_anonymous_cookie()
await config.add_cookie(new_anonymous_cookie)
async def add_user_cookie(self, content: str, cookie_name: str | None = None) -> Cookie:
"""添加用户 cookie"""
from ..platform import site_manager
cookie_site = cast(type[CookieSite], site_manager[cls._site_name])
if not await cls.validate_cookie(content):
if not await self.validate_cookie(content):
raise ValueError()
cookie = Cookie(site_name=cls._site_name, content=content)
cookie.cookie_name = cookie_site.get_cookie_name(content)
cookie.cd = cls._default_cd
await config.add_cookie(cookie)
cookie = Cookie(site_name=self._site_name, content=content)
cookie.cookie_name = cookie_name if cookie_name else await self.get_cookie_name(content)
cookie.cd = self._default_cookie_cd
cookie_id = await config.add_cookie(cookie)
return await config.get_cookie_by_id(cookie_id)
@classmethod
async def validate_cookie(cls, content: str) -> bool:
async def get_cookie_name(self, content: str) -> str:
"""从cookie内容中获取cookie的友好名字添加cookie时调用持久化在数据库中"""
from . import text_fletten
return text_fletten(f"{self._site_name} [{content[:10]}]")
async def validate_cookie(self, content: str) -> bool:
"""验证 cookie 内容是否有效,添加 cookie 时用,可根据平台的具体情况进行重写"""
# todo: 考虑移动到 cookie site 中
try:
data = json.loads(content)
if not isinstance(data, dict):
@ -84,7 +105,7 @@ class CookieClientManager(ClientManager):
return False
return True
def _generate_hook(self, cookie: Cookie) -> callable:
def _generate_hook(self, cookie: Cookie) -> Callable:
"""hook 函数生成器,用于回写请求状态到数据库"""
async def _response_hook(resp: httpx.Response):
@ -92,7 +113,7 @@ class CookieClientManager(ClientManager):
logger.trace(f"请求成功: {cookie.id} {resp.request.url}")
cookie.status = "success"
else:
logger.warning(f"请求失败:{cookie.id} {resp.request.url}, 状态码: {resp.status_code}")
logger.warning(f"请求失败: {cookie.id} {resp.request.url}, 状态码: {resp.status_code}")
cookie.status = "failed"
cookie.last_usage = datetime.now()
await config.update_cookie(cookie)
@ -118,7 +139,7 @@ class CookieClientManager(ClientManager):
return await self._assemble_client(client, cookie)
async def _assemble_client(self, client, cookie) -> AsyncClient:
"""组装 client可以自定义 cookie 对象的 content 装配到 client 中的方式"""
"""组装 client可以自定义 cookie 对象装配到 client 中的方式"""
cookies = httpx.Cookies()
if cookie:
cookies.update(json.loads(cookie.content))
@ -126,6 +147,15 @@ class CookieClientManager(ClientManager):
client.event_hooks = {"response": [self._generate_hook(cookie)]}
return client
@classmethod
def from_name(cls, site_name: str) -> type["CookieClientManager"]:
"""创建一个平台特化的 CookieClientManger"""
return type(
"CookieClientManager",
(CookieClientManager,),
{"_site_name": site_name},
)
async def get_client_for_static(self) -> AsyncClient:
return http_client()
@ -133,23 +163,32 @@ class CookieClientManager(ClientManager):
return http_client()
async def refresh_client(self):
pass
await self._refresh_anonymous_cookie()
def is_cookie_client_manager(manger: type[ClientManager]) -> bool:
return issubclass(manger, CookieClientManager)
def create_cookie_client_manager(site_name: str) -> type[CookieClientManager]:
"""创建一个平台特化的 CookieClientManger"""
return type(
"CookieClientManager",
(CookieClientManager,),
{"_site_name": site_name},
)
site_manager: dict[str, type["Site"]] = {}
class Site(metaclass=RegistryMeta, base=True):
class SiteMeta(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"):
# this is the base class
cls._key = kwargs.get("key")
elif not kwargs.get("abstract"):
# this is the subclass
if hasattr(cls, "name"):
site_manager[cls.name] = cls
super().__init__(name, bases, namespace, **kwargs)
class Site(metaclass=SiteMeta):
schedule_type: Literal["date", "interval", "cron"]
schedule_setting: dict
name: str
@ -161,18 +200,6 @@ class Site(metaclass=RegistryMeta, base=True):
return f"[{self.name}]-{self.name}-{self.schedule_setting}"
class CookieSite(Site):
client_mgr: type[CookieClientManager] = CookieClientManager
cookie_format_prompt = "无效的 Cookie请检查后重新输入详情见<待添加的文档>"
@classmethod
def get_cookie_name(cls, content: str) -> str:
"""从cookie内容中获取cookie的友好名字添加cookie时调用持久化在数据库中"""
from . import text_fletten
return text_fletten(f"{cls.name} [{content[:10]}]")
def anonymous_site(schedule_type: Literal["date", "interval", "cron"], schedule_setting: dict) -> type[Site]:
return type(
"AnonymousSite",
@ -183,7 +210,3 @@ def anonymous_site(schedule_type: Literal["date", "interval", "cron"], schedule_
"client_mgr": DefaultClientManager,
},
)
class SkipRequestException(Exception):
pass

View File

@ -11,9 +11,12 @@
"docs:update-package": "pnpm dlx vp-update"
},
"devDependencies": {
"@vuepress/bundler-vite": "2.0.0-rc.15",
"@vuepress/bundler-vite": "2.0.0-rc.17",
"mermaid": "^11.3.0",
"sass-embedded": "^1.79.5",
"vue": "^3.5.6",
"vuepress": "2.0.0-rc.15",
"vuepress-theme-hope": "2.0.0-rc.52"
"vuepress": "2.0.0-rc.17",
"vuepress-plugin-md-enhance": "2.0.0-rc.57",
"vuepress-theme-hope": "2.0.0-rc.58"
}
}

3672
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

8
poetry.lock generated
View File

@ -3149,13 +3149,13 @@ reference = "offical-source"
[[package]]
name = "pre-commit"
version = "3.8.0"
version = "4.0.1"
description = "A framework for managing and maintaining multi-language pre-commit hooks."
optional = false
python-versions = ">=3.9"
files = [
{file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
{file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
{file = "pre_commit-4.0.1-py2.py3-none-any.whl", hash = "sha256:efde913840816312445dc98787724647c65473daefe420785f885e8ed9a06878"},
{file = "pre_commit-4.0.1.tar.gz", hash = "sha256:80905ac375958c0444c65e9cebebd948b3cdb518f335a091a670a89d652139d2"},
]
[package.dependencies]
@ -5129,4 +5129,4 @@ yaml = []
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<4.0.0"
content-hash = "02cf0461830f2012ad533f01d152e7b4abb9e8bd94158956a73369f4f91c96b6"
content-hash = "49633514954cd83e3973b4539baa86ca5518d8582bfdb08df85247e5dcabc70f"

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "nonebot-bison"
version = "0.9.4"
version = "0.9.5"
description = "Subscribe message from social medias"
authors = ["felinae98 <felinae225@qq.com>"]
license = "MIT"
@ -49,7 +49,7 @@ ipdb = "^0.13.13"
isort = "^5.13.2"
nonemoji = "^0.1.4"
nb-cli = "^1.4.2"
pre-commit = "^3.8.0"
pre-commit = "^4.0.1"
ruff = "^0.6.5"
[tool.poetry.group.test.dependencies]

View File

@ -6,14 +6,15 @@ import pytest
from nonebug import App
@pytest.mark.usefixtures("_patch_weibo_get_cookie_name")
async def test_cookie(app: App, init_scheduler):
from nonebot_plugin_saa import TargetQQGroup
from nonebot_bison.platform import site_manager
from nonebot_bison.config.db_config import config
from nonebot_bison.scheduler import scheduler_dict
from nonebot_bison.types import Target as T_Target
from nonebot_bison.utils.site import CookieClientManager
from nonebot_bison.config.utils import DuplicateCookieTargetException
from nonebot_bison.utils.site import CookieClientManager, site_manager
target = T_Target("weibo_id")
platform_name = "weibo"
@ -26,14 +27,13 @@ async def test_cookie(app: App, init_scheduler):
tags=[],
)
site = site_manager["weibo.com"]
client_mgr = cast(CookieClientManager, site.client_mgr)
client_mgr = cast(CookieClientManager, scheduler_dict[site].client_mgr)
# 刷新匿名cookie
await client_mgr.refresh_anonymous_cookie()
await client_mgr.refresh_client()
cookies = await config.get_cookie(site_name=site.name)
assert len(cookies) == 1
# 添加用户cookie
await client_mgr.add_user_cookie(json.dumps({"test_cookie": "1"}))
await client_mgr.add_user_cookie(json.dumps({"test_cookie": "2"}))
@ -67,6 +67,7 @@ async def test_cookie(app: App, init_scheduler):
cats=[],
tags=[],
)
await client_mgr.add_user_cookie(json.dumps({"test_cookie": "3"}))
cookies = await config.get_cookie(site_name=site.name, is_anonymous=False)

View File

@ -29,6 +29,16 @@ def load_adapters(nonebug_init: None):
return driver
def patch_refresh_bilibili_anonymous_cookie(mocker: MockerFixture):
# patch 掉bilibili的匿名cookie生成函数避免真实请求
from nonebot_bison.platform.bilibili.scheduler import BilibiliClientManager
mocker.patch.object(
BilibiliClientManager, "_get_cookies", return_value=[{"name": "test anonymous", "content": "test"}]
)
@pytest.fixture
async def app(tmp_path: Path, request: pytest.FixtureRequest, mocker: MockerFixture):
sys.path.append(str(Path(__file__).parent.parent / "src" / "plugins"))
@ -51,6 +61,10 @@ async def app(tmp_path: Path, request: pytest.FixtureRequest, mocker: MockerFixt
param: AppReq = getattr(request, "param", AppReq())
# 如果在 app 前调用会报错“无法找到调用者”
# 而在后面调用又来不及mock所以只能在中间mock
patch_refresh_bilibili_anonymous_cookie(mocker)
if not param.get("no_init_db"):
await init_db()
# if not param.get("refresh_bot"):
@ -123,3 +137,22 @@ async def _no_browser(app: App, mocker: MockerFixture):
mocker.patch.object(plugin_config, "bison_use_browser", False)
mocker.patch("nonebot_bison.platform.unavailable_paltforms", _get_unavailable_platforms())
@pytest.fixture
async def _clear_db(app: App):
from nonebot_bison.config import config
await config.clear_db()
yield
await config.clear_db()
return
@pytest.fixture
def _patch_weibo_get_cookie_name(app: App, mocker: MockerFixture):
from nonebot_bison.platform import weibo
mocker.patch.object(weibo.WeiboClientManager, "_get_current_user_name", return_value="test_name")
yield
mocker.stopall()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,138 @@
{
"ok": 1,
"data": {
"config": {
"code": 11000,
"text": "<p>非微博会员不可多次修改昵称。自2024年1月1日至今您已成功修改1次目前无法继续修改。如需继续改名可开通微博会员。</p>",
"guide": {
"title": "开通微博会员",
"desc": "<p>本年度<span>增加最多8次</span>改名机会</p>",
"button_text": "开通会员",
"button_url": "https://m.weibo.cn/c/upgrade"
}
},
"data": {
"config": {
"title": "修改昵称次数扣除明细"
}
},
"user": {
"id": 114514,
"idstr": "114514",
"class": 1,
"screen_name": "suyiiyii",
"name": "suyiiyii",
"province": "44",
"city": "1",
"location": "广东 广州",
"description": "",
"url": "",
"profile_image_url": "https://tvax1.sinaimg.cn/default/images/default_avatar_male_50.gif?KID=imgbed,tva&Expires=1728833531&ssig=jURhYal3%2BR",
"light_ring": false,
"profile_url": "u/114514",
"domain": "",
"weihao": "",
"gender": "m",
"followers_count": 1,
"followers_count_str": "1",
"friends_count": 6,
"pagefriends_count": 0,
"statuses_count": 1,
"video_status_count": 0,
"video_play_count": 0,
"super_topic_not_syn_count": 0,
"favourites_count": 0,
"created_at": "Tue Sep 03 00:07:59 +0800 2024",
"following": false,
"allow_all_act_msg": false,
"geo_enabled": true,
"verified": false,
"verified_type": -1,
"remark": "",
"insecurity": {
"sexual_content": false
},
"ptype": 0,
"allow_all_comment": true,
"avatar_large": "https://tvax1.sinaimg.cn/default/images/default_avatar_male_180.gif?KID=imgbed,tva&Expires=1728833531&ssig=cornnikInk",
"avatar_hd": "https://tvax1.sinaimg.cn/default/images/default_avatar_male_180.gif?KID=imgbed,tva&Expires=1728833531&ssig=cornnikInk",
"verified_reason": "",
"verified_trade": "",
"verified_reason_url": "",
"verified_source": "",
"verified_source_url": "",
"follow_me": false,
"like": false,
"like_me": false,
"online_status": 0,
"bi_followers_count": 0,
"lang": "zh-cn",
"star": 0,
"mbtype": 0,
"mbrank": 0,
"svip": 0,
"vvip": 0,
"mb_expire_time": 0,
"block_word": 0,
"block_app": 0,
"chaohua_ability": 0,
"brand_ability": 0,
"nft_ability": 0,
"vplus_ability": 0,
"wenda_ability": 0,
"live_ability": 0,
"gongyi_ability": 0,
"paycolumn_ability": 0,
"newbrand_ability": 0,
"ecommerce_ability": 0,
"hardfan_ability": 0,
"wbcolumn_ability": 0,
"interaction_user": 0,
"audio_ability": 0,
"place_ability": 0,
"credit_score": 80,
"user_ability": 0,
"urank": 0,
"story_read_state": -1,
"vclub_member": 0,
"is_teenager": 0,
"is_guardian": 0,
"is_teenager_list": 0,
"pc_new": 0,
"special_follow": false,
"planet_video": 0,
"video_mark": 0,
"live_status": 0,
"user_ability_extend": 0,
"status_total_counter": {
"total_cnt": 0,
"repost_cnt": 0,
"comment_cnt": 0,
"like_cnt": 0,
"comment_like_cnt": 0
},
"video_total_counter": {
"play_cnt": -1
},
"brand_account": 0,
"hongbaofei": 0,
"green_mode": 0,
"urisk": 524288,
"unfollowing_recom_switch": 1,
"block": 0,
"block_me": 0,
"avatar_type": 0,
"is_big": 0,
"auth_status": 1,
"auth_realname": null,
"auth_career": null,
"auth_career_name": null,
"show_auth": 0,
"is_auth": 0,
"is_punish": 0,
"like_display": 0
},
"submit": true,
"having_count": 0
}
}

View File

@ -45,7 +45,7 @@ async def test_fetch_new(weibo, dummy_user_subinfo):
from nonebot_bison.types import Target, SubUnit
ak_list_router = respx.get("https://m.weibo.cn/api/container/getIndex?containerid=1076036279793937")
detail_router = respx.get("https://m.weibo.cn/statuses/show?id=4649031014551911")
detail_router = respx.get("https://m.weibo.cn/statuses/extend?id=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")))
image_cdn_router.mock(Response(200, content=b""))
@ -77,7 +77,7 @@ async def test_fetch_new(weibo, dummy_user_subinfo):
@pytest.mark.asyncio
@respx.mock
async def test_fetch_repost(weibo):
repost_detail_router = respx.get("https://m.weibo.cn/statuses/show?id=4645748019299849")
repost_detail_router = respx.get("https://m.weibo.cn/statuses/extend?id=4645748019299849")
repost_detail_router.mock(return_value=Response(200, text=get_file("weibo_detail_4645748019299849")))
image_cdn_router.mock(Response(200, content=b""))
raw_post = get_json("weibo_ak_list_1.json")["data"]["cards"][3]
@ -121,7 +121,7 @@ async def test_fetch_repost(weibo):
@pytest.mark.asyncio
@respx.mock
async def test_video_cover(weibo):
router = respx.get("https://m.weibo.cn/statuses/show?id=4645748019299849")
router = respx.get("https://m.weibo.cn/statuses/extend?id=4645748019299849")
router.mock(return_value=Response(200, text=get_file("weibo_detail_4645748019299849")))
image_cdn_router.mock(Response(200, content=b""))
raw_post = get_json("weibo_ak_list_1.json")["data"]["cards"][0]
@ -152,7 +152,7 @@ async def test_classification(weibo):
@pytest.mark.asyncio
@respx.mock
async def test_parse_long(weibo):
detail_router = respx.get("https://m.weibo.cn/statuses/show?id=4645748019299849")
detail_router = respx.get("https://m.weibo.cn/statuses/extend?id=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]
post = await weibo.parse(raw_post)
@ -217,3 +217,14 @@ async def test_parse_target(weibo: "Weibo"):
assert res == "6441489862"
with pytest.raises(Platform.ParseTargetException):
await weibo.parse_target("https://weibo.com/arknights")
@respx.mock
async def test_get_cookie_name(weibo: "Weibo"):
from nonebot_bison.platform.weibo import WeiboClientManager
router = respx.get("https://m.weibo.cn/setup/nick/detail")
router.mock(return_value=Response(200, json=get_json("weibo_get-cookie-name.json")))
weibo_client_mgr = WeiboClientManager()
name = await weibo_client_mgr.get_cookie_name("{}")
assert name == "weibo: [suyiiyii]"

View File

@ -5,6 +5,8 @@ from unittest.mock import AsyncMock
from nonebug import App
from pytest_mock import MockerFixture
from tests.conftest import patch_refresh_bilibili_anonymous_cookie
if typing.TYPE_CHECKING:
from nonebot_bison.utils import Site
@ -199,6 +201,7 @@ async def test_scheduler_skip_browser(mocker: MockerFixture):
site = MockSite
mocker.patch.dict(platform_manager, {"mock_platform": MockPlatform})
patch_refresh_bilibili_anonymous_cookie(mocker)
await init_scheduler()
@ -229,6 +232,7 @@ async def test_scheduler_no_skip_not_require_browser(mocker: MockerFixture):
site = MockSite
mocker.patch.dict(platform_manager, {"mock_platform": MockPlatform})
patch_refresh_bilibili_anonymous_cookie(mocker)
await init_scheduler()

View File

@ -1,5 +1,6 @@
import json
import pytest
from nonebug.app import App
from pytest_mock import MockerFixture
@ -30,7 +31,8 @@ async def test_add_cookie_rule(app: App, mocker: MockerFixture):
ctx.should_pass_permission()
async def test_add_cookie_target_no_cookie(app: App, mocker: MockerFixture):
@pytest.mark.usefixtures("_clear_db")
async def test_add_cookie_target_no_cookie(app: App):
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
@ -80,7 +82,9 @@ async def test_add_cookie_target_no_cookie(app: App, mocker: MockerFixture):
)
async def test_add_cookie(app: App, mocker: MockerFixture):
@pytest.mark.usefixtures("_clear_db")
@pytest.mark.usefixtures("_patch_weibo_get_cookie_name")
async def test_add_cookie(app: App):
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
@ -90,7 +94,7 @@ async def test_add_cookie(app: App, mocker: MockerFixture):
async with app.test_matcher(add_cookie_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
event_1 = fake_private_message_event(
message=Message("添加Cookie"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
message=Message("添加cookie"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
ctx.receive_event(bot, event_1)
ctx.should_pass_rule()
@ -120,7 +124,11 @@ async def test_add_cookie(app: App, mocker: MockerFixture):
message=Message("test"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
ctx.receive_event(bot, event_4_err)
ctx.should_call_send(event_4_err, "无效的 Cookie请检查后重新输入详情见<待添加的文档>", True)
ctx.should_call_send(
event_4_err,
"无效的 Cookie请检查后重新输入详情见https://nonebot-bison.netlify.app/usage/cookie.html",
True,
)
ctx.should_rejected()
event_4_ok = fake_private_message_event(
message=Message(json.dumps({"cookie": "test"})),
@ -131,7 +139,7 @@ async def test_add_cookie(app: App, mocker: MockerFixture):
ctx.receive_event(bot, event_4_ok)
ctx.should_pass_rule()
ctx.should_call_send(
event_4_ok, '已添加 Cookie: {"cookie": "test"} 到平台 weibo\n请使用“关联cookie”为 Cookie 关联订阅', True
event_4_ok, "已添加 Cookie: weibo: [test_name] 到平台 weibo\n请使用“关联cookie”为 Cookie 关联订阅", True
)
async with app.test_matcher(add_cookie_target_matcher) as ctx:
@ -176,7 +184,7 @@ async def test_add_cookie(app: App, mocker: MockerFixture):
)
ctx.receive_event(bot, event_2_ok)
ctx.should_pass_rule()
ctx.should_call_send(event_2_ok, '请选择一个 Cookie已关联的 Cookie 不会显示\n1. weibo.com [{"cookie":]', True)
ctx.should_call_send(event_2_ok, "请选择一个 Cookie已关联的 Cookie 不会显示\n1. weibo: [test_name]", True)
event_3_err = fake_private_message_event(
message=Message("2"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
@ -188,11 +196,10 @@ async def test_add_cookie(app: App, mocker: MockerFixture):
)
ctx.receive_event(bot, event_3_ok)
ctx.should_pass_rule()
ctx.should_call_send(event_3_ok, '已关联 Cookie: weibo.com [{"cookie":] 到订阅 weibo.com weibo_id', True)
ctx.should_call_send(event_3_ok, "已关联 Cookie: weibo: [test_name] 到订阅 weibo.com weibo_id", True)
async def test_add_cookie_target_no_target(app: App, mocker: MockerFixture):
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message

View File

@ -1,10 +1,73 @@
import json
import pytest
from nonebug.app import App
from ..utils import fake_superuser, fake_private_message_event
@pytest.mark.usefixtures("_clear_db")
async def test_del_cookie(app: App):
from nonebug_saa import should_send_saa
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebot_plugin_saa import TargetQQGroup, MessageFactory
from nonebot_bison.config import config
from nonebot_bison.config.db_model import Cookie
from nonebot_bison.types import Target as T_Target
from nonebot_bison.sub_manager import del_cookie_matcher
async with app.test_matcher(del_cookie_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
event = fake_private_message_event(
message=Message("删除cookie"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
ctx.receive_event(bot, event)
ctx.should_pass_rule()
ctx.should_pass_permission()
ctx.should_call_send(event, "暂无已添加的 Cookie\n请使用“添加cookie”命令添加", True)
async with app.test_matcher(del_cookie_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
target = T_Target("weibo_id")
platform_name = "weibo"
await config.add_subscribe(
TargetQQGroup(group_id=123),
target=target,
target_name="weibo_name",
platform_name=platform_name,
cats=[],
tags=[],
)
await config.add_cookie(Cookie(content=json.dumps({"cookie": "test"}), site_name="weibo.com"))
event_1 = fake_private_message_event(
message=Message("删除cookie"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
ctx.receive_event(bot, event_1)
ctx.should_pass_rule()
ctx.should_pass_permission()
should_send_saa(
ctx,
MessageFactory(
"已添加的 Cookie 为:\n1 weibo.com unnamed cookie"
" 0个关联\n请输入要删除的 Cookie 的序号\n输入'取消'中止"
),
bot,
event=event_1,
)
event_2 = fake_private_message_event(
message=Message("1"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
ctx.receive_event(bot, event_2)
ctx.should_pass_rule()
ctx.should_pass_permission()
ctx.should_call_send(event_2, "删除成功", True)
@pytest.mark.usefixtures("_clear_db")
@pytest.mark.usefixtures("_patch_weibo_get_cookie_name")
async def test_del_cookie_err(app: App):
from nonebug_saa import should_send_saa
from nonebot.adapters.onebot.v11.bot import Bot
@ -51,8 +114,7 @@ async def test_del_cookie_err(app: App):
should_send_saa(
ctx,
MessageFactory(
'已添加的 Cookie 为:\n1 weibo.com weibo.com [{"cookie":] '
"1个关联\n请输入要删除的 Cookie 的序号\n输入'取消'中止"
"已添加的 Cookie 为:\n1 weibo.com unnamed cookie 1个关联\n请输入要删除的 Cookie 的序号\n输入'取消'中止"
),
bot,
event=event_1,
@ -72,62 +134,3 @@ async def test_del_cookie_err(app: App):
ctx.should_call_send(event_2, "只能删除未关联的 Cookie请使用“取消关联cookie”命令取消关联", True)
ctx.should_call_send(event_2, "删除错误", True)
ctx.should_rejected()
async def test_del_cookie(app: App):
from nonebug_saa import should_send_saa
from nonebot.adapters.onebot.v11.bot import Bot
from nonebot.adapters.onebot.v11.message import Message
from nonebot_plugin_saa import TargetQQGroup, MessageFactory
from nonebot_bison.config import config
from nonebot_bison.config.db_model import Cookie
from nonebot_bison.types import Target as T_Target
from nonebot_bison.sub_manager import del_cookie_matcher
async with app.test_matcher(del_cookie_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
event = fake_private_message_event(
message=Message("删除cookie"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
ctx.receive_event(bot, event)
ctx.should_pass_rule()
ctx.should_pass_permission()
ctx.should_call_send(event, "暂无已添加的 Cookie\n请使用“添加cookie”命令添加", True)
async with app.test_matcher(del_cookie_matcher) as ctx:
bot = ctx.create_bot(base=Bot)
target = T_Target("weibo_id")
platform_name = "weibo"
await config.add_subscribe(
TargetQQGroup(group_id=123),
target=target,
target_name="weibo_name",
platform_name=platform_name,
cats=[],
tags=[],
)
await config.add_cookie(Cookie(content=json.dumps({"cookie": "test"}), site_name="weibo.com"))
event_1 = fake_private_message_event(
message=Message("删除cookie"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
ctx.receive_event(bot, event_1)
ctx.should_pass_rule()
ctx.should_pass_permission()
should_send_saa(
ctx,
MessageFactory(
'已添加的 Cookie 为:\n1 weibo.com weibo.com [{"cookie":]'
" 0个关联\n请输入要删除的 Cookie 的序号\n输入'取消'中止"
),
bot,
event=event_1,
)
event_2 = fake_private_message_event(
message=Message("1"), sender=fake_superuser, to_me=True, user_id=fake_superuser.user_id
)
ctx.receive_event(bot, event_2)
ctx.should_pass_rule()
ctx.should_pass_permission()
ctx.should_call_send(event_2, "删除成功", True)

View File

@ -0,0 +1,69 @@
{
"version": 3,
"groups": [
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 1232
},
"subs": [
{
"categories": [],
"tags": [],
"target": {
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
}
]
},
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 2342
},
"subs": [
{
"categories": [],
"tags": ["kaltsit", "amiya"],
"target": {
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
},
{
"categories": [1, 2],
"tags": [],
"target": {
"target_name": "bilibili_name",
"target": "bilibili_id",
"platform_name": "bilibili",
"default_schedule_weight": 10
}
}
]
}
],
"cookies": [
{
"site_name": "weibo.com",
"content": "{\"cookie\": \"test\"}",
"cookie_name": "test cookie",
"cd_milliseconds": 0,
"is_universal": false,
"tags": {},
"targets": [
{
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
]
}
]
}

View File

@ -0,0 +1,48 @@
version: 3
groups:
- subs:
- categories: []
tags: []
target:
default_schedule_weight: 10
platform_name: weibo
target: weibo_id
target_name: weibo_name
user_target:
platform_type: QQ Group
group_id: 123552
- subs:
- categories: []
tags:
- kaltsit
- amiya
target:
default_schedule_weight: 10
platform_name: weibo
target: weibo_id
target_name: weibo_name
- categories:
- 1
- 2
tags: []
target:
default_schedule_weight: 10
platform_name: bilibili
target: bilibili_id
target_name: bilibili_name
user_target:
platform_type: QQ Group
group_id: 234662
cookies:
- site_name: weibo.com
content: '{"cookie": "test"}'
cookie_name: test cookie
cd_milliseconds: 0
is_universal: false
tags: {}
targets:
- target_name: weibo_name
target: weibo_id
platform_name: weibo
default_schedule_weight: 10

View File

@ -0,0 +1,103 @@
{
"version": 2,
"groups": [
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 123
},
"subs": [
{
"categories": [],
"tags": [],
"target": {
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
}
]
},
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 234
},
"subs": [
{
"tags": ["kaltsit", "amiya"],
"target": {
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
},
{
"categories": [1, 2],
"tags": [],
"target": [
{
"target_name": "bilibili_name",
"target": "bilibili_id",
"platform_name": "bilibili",
"default_schedule_weight": 10
}
]
}
]
},
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 123
},
"subs": {
"categories": [],
"tags": [],
"target": {
"target_name": "weibo_name2",
"target": "weibo_id2",
"platform_name": "weibo",
"default_schedule_weight": 10
}
}
},
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 123
},
"subs": [
{
"categories": [],
"tags": [],
"target": {
"target_name": "weibo_name2",
"platform_name": "weibo",
"default_schedule_weight": 10
}
}
]
}
],
"cookies": [
{
"site_name": "weibo.com1111",
"content": "{\"cookie\": 111}",
"cookie_name": "test cookie1",
"cd_milliseconds": -1,
"is_universal": false,
"tags": {},
"targets": [
{
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
]
}
]
}

View File

@ -0,0 +1,97 @@
{
"version": 3,
"groups": [
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 1232
},
"subs": [
{
"categories": [],
"tags": [],
"target": {
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
}
]
},
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 2342
},
"subs": [
{
"categories": [],
"tags": ["kaltsit", "amiya"],
"target": {
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
},
{
"categories": [1, 2],
"tags": [],
"target": {
"target_name": "bilibili_name",
"target": "bilibili_id",
"platform_name": "bilibili",
"default_schedule_weight": 10
}
}
]
},
{
"user_target": {
"platform_type": "QQ Group",
"group_id": 1232
},
"subs": [
{
"categories": [],
"tags": [],
"target": {
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
},
{
"categories": [2, 6],
"tags": ["poca"],
"target": {
"target_name": "weibo_name2",
"target": "weibo_id2",
"platform_name": "weibo",
"default_schedule_weight": 10
}
}
]
}
],
"cookies": [
{
"site_name": "weibo.com",
"content": "{\"cookie\": \"test\"}",
"cookie_name": "test cookie",
"cd_milliseconds": 0,
"is_universal": false,
"tags": {},
"targets": [
{
"target_name": "weibo_name",
"target": "weibo_id",
"platform_name": "weibo",
"default_schedule_weight": 10
}
]
}
]
}

View File

@ -40,6 +40,7 @@ def test_cli_help(app: App):
async def test_subs_export(app: App, tmp_path: Path):
from nonebot_plugin_saa import TargetQQGroup
from nonebot_bison.config.db_model import Cookie
from nonebot_bison.config.db_config import config
from nonebot_bison.types import Target as TTarget
from nonebot_bison.script.cli import cli, run_sync
@ -70,6 +71,14 @@ async def test_subs_export(app: App, tmp_path: Path):
cats=[1, 2],
tags=[],
)
cookie_id = await config.add_cookie(
Cookie(
site_name="weibo.com",
content='{"cookie": "test"}',
cookie_name="test cookie",
)
)
await config.add_cookie_target("weibo_id", "weibo", cookie_id)
assert len(await config.list_subs_with_all_info()) == 3
@ -84,8 +93,9 @@ async def test_subs_export(app: App, tmp_path: Path):
assert result.exit_code == 0
file_path = Path.cwd() / "bison_subscribes_export_1.json"
assert file_path.exists()
assert '"version": 2' in file_path.read_text()
assert '"version": 3' in file_path.read_text()
assert '"group_id": 123' in file_path.read_text()
assert '"content": "{\\"cookie\\": \\"test\\"}",\n' in file_path.read_text()
# 是否导出到指定已存在文件夹
data_dir = tmp_path / "data"
@ -94,8 +104,9 @@ async def test_subs_export(app: App, tmp_path: Path):
assert result.exit_code == 0
file_path2 = data_dir / "bison_subscribes_export_1.json"
assert file_path2.exists()
assert '"version": 2' in file_path2.read_text()
assert '"version": 3' in file_path2.read_text()
assert '"group_id": 123' in file_path2.read_text()
assert '"content": "{\\"cookie\\": \\"test\\"}",\n' in file_path.read_text()
# 是否拒绝导出到不存在的文件夹
result = await run_sync(runner.invoke)(cli, ["export", "-p", str(tmp_path / "data2")])
@ -106,9 +117,10 @@ async def test_subs_export(app: App, tmp_path: Path):
assert result.exit_code == 0
file_path3 = tmp_path / "bison_subscribes_export_1.yaml"
assert file_path3.exists()
assert "version: 2" in file_path3.read_text()
assert "version: 3" in file_path3.read_text()
assert "group_id: 123" in file_path3.read_text()
assert "platform_type: QQ Group" in file_path3.read_text()
assert '"content": "{\\"cookie\\": \\"test\\"}",\n' in file_path.read_text()
# 是否允许以未支持的格式导出
result = await run_sync(runner.invoke)(cli, ["export", "-p", str(tmp_path), "--format", "toml"])

View File

@ -5,12 +5,13 @@ from nonebot.compat import model_dump
from .utils import get_json
@pytest.mark.usefixtures("_clear_db")
async def test_subs_export(app: App, init_scheduler):
from nonebot_plugin_saa import TargetQQGroup
from nonebot_bison.config.db_model import User
from nonebot_bison.config.db_config import config
from nonebot_bison.types import Target as TTarget
from nonebot_bison.config.db_model import User, Cookie
from nonebot_bison.config.subs_io import subscribes_export
await config.add_subscribe(
@ -37,12 +38,20 @@ async def test_subs_export(app: App, init_scheduler):
cats=[1, 2],
tags=[],
)
cookie_id = await config.add_cookie(
Cookie(
site_name="weibo.com",
content='{"cookie": "test"}',
cookie_name="test cookie",
)
)
await config.add_cookie_target("weibo_id", "weibo", cookie_id)
data = await config.list_subs_with_all_info()
assert len(data) == 3
nbesf_data = await subscribes_export(lambda x: x)
assert model_dump(nbesf_data) == get_json("v2/subs_export.json")
assert model_dump(nbesf_data) == get_json("v3/subs_export.json")
nbesf_data_user_234 = await subscribes_export(
lambda stmt: stmt.where(User.user_target == {"platform_type": "QQ Group", "group_id": 2342})
@ -102,16 +111,30 @@ async def test_subs_import_dup_err(app: App, init_scheduler):
async def test_subs_import_version_disorder(app: App, init_scheduler):
from nonebot_bison.config.subs_io import subscribes_import
from nonebot_bison.config.subs_io.nbesf_model import v1, v2
from nonebot_bison.config.subs_io.utils import NBESFParseErr
# use v2 parse v1
with pytest.raises(NBESFParseErr):
v1.nbesf_parser(get_json("v2/subs_export_has_subdup_err.json"))
from nonebot_bison.config.subs_io.nbesf_model import v1, v2, v3
# use v1 parse v2
with pytest.raises(NBESFParseErr):
v1.nbesf_parser(get_json("v2/subs_export_has_subdup_err.json"))
# use v1 parse v3
with pytest.raises(NBESFParseErr):
v1.nbesf_parser(get_json("v3/subs_export_has_subdup_err.json"))
# use v2 parse v1
with pytest.raises(NBESFParseErr):
v2.nbesf_parser(get_json("v1/subs_export_has_subdup_err.json"))
# # use v2 parse v3
# with pytest.raises(NBESFParseErr):
# v2.nbesf_parser(get_json("v3/subs_export_has_subdup_err.json"))
# use v3 parse v1
with pytest.raises(NBESFParseErr):
v3.nbesf_parser(get_json("v1/subs_export_has_subdup_err.json"))
# # use v3 parse v2
# with pytest.raises(NBESFParseErr):
# v3.nbesf_parser(get_json("v2/subs_export_has_subdup_err.json"))
# TODO: v3 parse v2 不会报错但是v3 parse v1 会报错,似乎是有问题 (
with pytest.raises(AssertionError): # noqa: PT012
nbesf_data = v2.nbesf_parser(get_json("v2/subs_export_has_subdup_err.json"))
@ -121,7 +144,7 @@ async def test_subs_import_version_disorder(app: App, init_scheduler):
async def test_subs_import_all_fail(app: App, init_scheduler):
"""只要文件格式有任何一个错误, 都不会进行订阅"""
from nonebot_bison.config.subs_io.nbesf_model import v1, v2
from nonebot_bison.config.subs_io.nbesf_model import v1, v2, v3
from nonebot_bison.config.subs_io.nbesf_model.v1 import NBESFParseErr
with pytest.raises(NBESFParseErr):
@ -129,3 +152,6 @@ async def test_subs_import_all_fail(app: App, init_scheduler):
with pytest.raises(NBESFParseErr):
v2.nbesf_parser(get_json("v2/subs_export_all_illegal.json"))
with pytest.raises(NBESFParseErr):
v3.nbesf_parser(get_json("v3/subs_export_all_illegal.json"))

View File

@ -17,7 +17,8 @@ async def test_http_error(app: App):
assert ctx.gen_req_records() == [
"https://example.com Headers({'host': 'example.com', 'accept': '*/*', 'accept-encoding': 'gzip, deflate',"
" 'connection': 'keep-alive', 'user-agent': 'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like"
" Gecko) Chrome/51.0.2704.103 Safari/537.36'}) | [403] Headers({'content-length': '15', 'content-type':"
" 'connection': 'keep-alive', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
" (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/130.0.0.0'}) | [403] Headers({'content-length': '"
"15', 'content-type':"
' \'application/json\'}) {"error": "gg"}'
]