Merge branch 'main' into nonebot-b1

This commit is contained in:
felinae98 2022-02-12 11:21:20 +08:00
commit 79799e6d44
No known key found for this signature in database
GPG Key ID: 00C8B010587FF610
52 changed files with 959 additions and 708 deletions

View File

@ -5,10 +5,11 @@ ci:
autoupdate_schedule: weekly autoupdate_schedule: weekly
autoupdate_commit_msg: "auto update by pre-commit hooks" autoupdate_commit_msg: "auto update by pre-commit hooks"
repos: repos:
- repo: https://github.com/pycqa/isort # - repo: https://github.com/pycqa/isort
rev: 5.10.1 # rev: 5.10.1
hooks: # hooks:
- id: isort # - id: isort
# args: ["--profile", "black", "--filter-files"]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 22.1.0 rev: 22.1.0
@ -19,4 +20,4 @@ repos:
rev: v2.5.1 rev: v2.5.1
hooks: hooks:
- id: prettier - id: prettier
types_or: [markdown] types_or: [markdown, ts, tsx]

View File

@ -51,3 +51,9 @@
- 增加过滤 nonebot 日志功能 - 增加过滤 nonebot 日志功能
- 前端可以刷新了(之前居然不可以) - 前端可以刷新了(之前居然不可以)
- 在镜像里塞进了浏览器(导致镜像体积起飞) - 在镜像里塞进了浏览器(导致镜像体积起飞)
## [0.4.4]
- 又双叒叕重构了一下
- 修复了 Docker 中 Playwright 下载的浏览器版本不正确问题
- 加入了猴子补丁,使 Windows 里能运行 Playwright

View File

@ -28,7 +28,7 @@
支持的平台: 支持的平台:
- 微博 - 微博
- B - Bilibili
- RSS - RSS
- 明日方舟 - 明日方舟
- 塞壬唱片新闻 - 塞壬唱片新闻

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from "react";
import { render, screen } from '@testing-library/react'; import { render, screen } from "@testing-library/react";
import App from './App'; import App from "./App";
test('renders learn react link', () => { test("renders learn react link", () => {
render(<App />); render(<App />);
const linkElement = screen.getByText(/learn react/i); const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument(); expect(linkElement).toBeInTheDocument();

View File

@ -1,37 +1,33 @@
import 'antd/dist/antd.css'; import "antd/dist/antd.css";
import React, {useEffect} from 'react'; import React, { useEffect } from "react";
import {useDispatch, useSelector} from 'react-redux'; import { useDispatch, useSelector } from "react-redux";
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'; import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import './App.css'; import "./App.css";
import {Admin} from './pages/admin'; import { Admin } from "./pages/admin";
import {Auth} from './pages/auth'; import { Auth } from "./pages/auth";
import {getGlobalConf} from './store/globalConfSlice'; import { getGlobalConf } from "./store/globalConfSlice";
import {useAppSelector} from './store/hooks'; import { useAppSelector } from "./store/hooks";
import {loadLoginState, loginSelector} from './store/loginSlice'; import { loadLoginState, loginSelector } from "./store/loginSlice";
function LoginSwitch() { function LoginSwitch() {
const login = useSelector(loginSelector) const login = useSelector(loginSelector);
if (login.login) { if (login.login) {
return <Admin />; return <Admin />;
} else { } else {
return ( return <div>not login</div>;
<div>
not login
</div>
)
} }
} }
function App() { function App() {
const dispatch = useDispatch() const dispatch = useDispatch();
const globalConf = useAppSelector(state => state.globalConf) const globalConf = useAppSelector((state) => state.globalConf);
useEffect(() => { useEffect(() => {
dispatch(getGlobalConf()); dispatch(getGlobalConf());
dispatch(loadLoginState()) dispatch(loadLoginState());
}, [dispatch]); }, [dispatch]);
return <> return (
{ globalConf.loaded && <>
{globalConf.loaded && (
<Router basename="/bison"> <Router basename="/bison">
<Switch> <Switch>
<Route path="/auth/:code"> <Route path="/auth/:code">
@ -42,8 +38,9 @@ function App() {
</Route> </Route>
</Switch> </Switch>
</Router> </Router>
} )}
</>; </>
);
} }
export default App; export default App;

View File

@ -1,6 +1,12 @@
import axios from "axios"; import axios from "axios";
import { GlobalConf, TokenResp, SubscribeResp, TargetNameResp, SubscribeConfig } from "../utils/type"; import {
import { baseUrl } from './utils'; GlobalConf,
TokenResp,
SubscribeResp,
TargetNameResp,
SubscribeConfig,
} from "../utils/type";
import { baseUrl } from "./utils";
export async function getGlobalConf(): Promise<GlobalConf> { export async function getGlobalConf(): Promise<GlobalConf> {
const res = await axios.get<GlobalConf>(`${baseUrl}global_conf`); const res = await axios.get<GlobalConf>(`${baseUrl}global_conf`);
@ -8,7 +14,9 @@ export async function getGlobalConf(): Promise<GlobalConf> {
} }
export async function auth(token: string): Promise<TokenResp> { export async function auth(token: string): Promise<TokenResp> {
const res = await axios.get<TokenResp>(`${baseUrl}auth`, {params: {token}}); const res = await axios.get<TokenResp>(`${baseUrl}auth`, {
params: { token },
});
return res.data; return res.data;
} }
@ -17,22 +25,39 @@ export async function getSubscribe(): Promise<SubscribeResp> {
return res.data; return res.data;
} }
export async function getTargetName(platformName: string, target: string): Promise<TargetNameResp> { export async function getTargetName(
const res = await axios.get(`${baseUrl}target_name`, {params: {platformName, target}}); platformName: string,
target: string
): Promise<TargetNameResp> {
const res = await axios.get(`${baseUrl}target_name`, {
params: { platformName, target },
});
return res.data; return res.data;
} }
export async function addSubscribe(groupNumber: string, req: SubscribeConfig) { export async function addSubscribe(groupNumber: string, req: SubscribeConfig) {
const res = await axios.post(`${baseUrl}subs`, req, {params: {groupNumber}}) const res = await axios.post(`${baseUrl}subs`, req, {
params: { groupNumber },
});
return res.data; return res.data;
} }
export async function delSubscribe(groupNumber: string, platformName: string, target: string) { export async function delSubscribe(
const res = await axios.delete(`${baseUrl}subs`, {params: {groupNumber, platformName, target}}); groupNumber: string,
platformName: string,
target: string
) {
const res = await axios.delete(`${baseUrl}subs`, {
params: { groupNumber, platformName, target },
});
return res.data; return res.data;
} }
export async function updateSubscribe(groupNumber: string, req: SubscribeConfig) { export async function updateSubscribe(
return axios.patch(`${baseUrl}subs`, req, {params: {groupNumber}}) groupNumber: string,
.then(res => res.data); req: SubscribeConfig
) {
return axios
.patch(`${baseUrl}subs`, req, { params: { groupNumber } })
.then((res) => res.data);
} }

View File

@ -1,32 +1,40 @@
import axios, {AxiosError} from "axios"; import axios, { AxiosError } from "axios";
import {Store} from "src/store"; import { Store } from "src/store";
import { clearLoginStatus } from 'src/store/loginSlice'; import { clearLoginStatus } from "src/store/loginSlice";
// import { useContext } from 'react'; // import { useContext } from 'react';
// import { LoginContext } from "../utils/context"; // import { LoginContext } from "../utils/context";
export const baseUrl = '/bison/api/' export const baseUrl = "/bison/api/";
let store: Store let store: Store;
export const injectStore = (_store: Store) => { export const injectStore = (_store: Store) => {
store = _store store = _store;
} };
// const loginStatus = useContext(LoginContext); // const loginStatus = useContext(LoginContext);
axios.interceptors.request.use(function (config) { axios.interceptors.request.use(
if (config.url && config.url.startsWith(baseUrl) && config.url !== `${baseUrl}auth` function (config) {
&& config.url !== `${baseUrl}global_conf`) { if (
const token = localStorage.getItem('token'); config.url &&
config.url.startsWith(baseUrl) &&
config.url !== `${baseUrl}auth` &&
config.url !== `${baseUrl}global_conf`
) {
const token = localStorage.getItem("token");
if (token) { if (token) {
config.headers['Authorization'] = `Bearer ${token}`; config.headers["Authorization"] = `Bearer ${token}`;
} else { } else {
throw new axios.Cancel('User not login'); throw new axios.Cancel("User not login");
} }
} }
return config; return config;
}, function (error) { },
function (error) {
return Promise.reject(error); return Promise.reject(error);
}); }
);
axios.interceptors.response.use(function (response) { axios.interceptors.response.use(
function (response) {
// const data = response.data; // const data = response.data;
// const parseToMap = (item: any): any => { // const parseToMap = (item: any): any => {
// if (item instanceof Array) { // if (item instanceof Array) {
@ -43,9 +51,11 @@ axios.interceptors.response.use(function (response) {
// } // }
// response.data = parseToMap(data); // response.data = parseToMap(data);
return response; return response;
}, function(error: AxiosError) { },
if(error.response && error.response.status === 401) { function (error: AxiosError) {
if (error.response && error.response.status === 401) {
store.dispatch(clearLoginStatus()); store.dispatch(clearLoginStatus());
} }
return Promise.reject(error); return Promise.reject(error);
}); }
);

View File

@ -1,15 +1,15 @@
import {Form, Input, Modal, Select, Tag} from 'antd'; import { Form, Input, Modal, Select, Tag } from "antd";
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import {useSelector} from 'react-redux'; import { useSelector } from "react-redux";
import {addSubscribe, getTargetName, updateSubscribe} from 'src/api/config'; import { addSubscribe, getTargetName, updateSubscribe } from "src/api/config";
import {InputTag} from 'src/component/inputTag'; import { InputTag } from "src/component/inputTag";
import {platformConfSelector} from 'src/store/globalConfSlice'; import { platformConfSelector } from "src/store/globalConfSlice";
import {CategoryConfig, SubscribeConfig} from 'src/utils/type'; import { CategoryConfig, SubscribeConfig } from "src/utils/type";
interface InputTagCustomProp { interface InputTagCustomProp {
value?: Array<string>, value?: Array<string>;
onChange?: (value: Array<string>) => void, onChange?: (value: Array<string>) => void;
disabled?: boolean disabled?: boolean;
} }
function InputTagCustom(prop: InputTagCustomProp) { function InputTagCustom(prop: InputTagCustomProp) {
const [value, setValue] = useState(prop.value || []); const [value, setValue] = useState(prop.value || []);
@ -18,165 +18,202 @@ function InputTagCustom(prop: InputTagCustomProp) {
if (prop.onChange) { if (prop.onChange) {
prop.onChange(newVal); prop.onChange(newVal);
} }
} };
useEffect(() => { useEffect(() => {
if (prop.value) { if (prop.value) {
setValue(prop.value); setValue(prop.value);
} }
}, [prop.value]) }, [prop.value]);
return ( return (
<> <>
{ {prop.disabled ? (
prop.disabled ? <Tag color="default"></Tag>: <Tag color="default"></Tag>
) : (
<> <>
{value.length === 0 && {value.length === 0 && <Tag color="green"></Tag>}
<Tag color="green"></Tag> <InputTag
} color="blue"
<InputTag color="blue" addText="添加标签" value={value} onChange={handleSetValue} /> addText="添加标签"
value={value}
onChange={handleSetValue}
/>
</> </>
} )}
</> </>
) );
} }
interface AddModalProp { interface AddModalProp {
showModal: boolean, showModal: boolean;
groupNumber: string, groupNumber: string;
setShowModal: (s: boolean) => void, setShowModal: (s: boolean) => void;
refresh: () => void refresh: () => void;
initVal?: SubscribeConfig initVal?: SubscribeConfig;
} }
export function AddModal({ export function AddModal({
showModal, groupNumber, setShowModal, refresh, initVal showModal,
groupNumber,
setShowModal,
refresh,
initVal,
}: AddModalProp) { }: AddModalProp) {
const [ confirmLoading, setConfirmLoading ] = useState<boolean>(false); const [confirmLoading, setConfirmLoading] = useState<boolean>(false);
const platformConf = useSelector(platformConfSelector) const platformConf = useSelector(platformConfSelector);
const [ hasTarget, setHasTarget ] = useState(false); const [hasTarget, setHasTarget] = useState(false);
const [ categories, setCategories ] = useState({} as CategoryConfig); const [categories, setCategories] = useState({} as CategoryConfig);
const [ enabledTag, setEnableTag ] = useState(false); const [enabledTag, setEnableTag] = useState(false);
const [ form ] = Form.useForm(); const [form] = Form.useForm();
const [ inited, setInited ] = useState(false); const [inited, setInited] = useState(false);
const changePlatformSelect = (platform: string) => { const changePlatformSelect = (platform: string) => {
setHasTarget(_ => platformConf[platform].hasTarget); setHasTarget((_) => platformConf[platform].hasTarget);
setCategories(_ => platformConf[platform].categories); setCategories((_) => platformConf[platform].categories);
setEnableTag(platformConf[platform].enabledTag) setEnableTag(platformConf[platform].enabledTag);
if (! platformConf[platform].hasTarget) { if (!platformConf[platform].hasTarget) {
getTargetName(platform, 'default') getTargetName(platform, "default").then((res) => {
.then(res => { console.log(res);
console.log(res)
form.setFieldsValue({ form.setFieldsValue({
targetName: res.targetName, targetName: res.targetName,
target: '' target: "",
}) });
}) });
} else { } else {
form.setFieldsValue({ form.setFieldsValue({
targetName: '', targetName: "",
target: '' target: "",
}) });
}
} }
};
const handleSubmit = (value: any) => { const handleSubmit = (value: any) => {
let newVal = Object.assign({}, value) let newVal = Object.assign({}, value);
if (typeof newVal.tags !== 'object') { if (typeof newVal.tags !== "object") {
newVal.tags = [] newVal.tags = [];
} }
if (typeof newVal.cats !== 'object') { if (typeof newVal.cats !== "object") {
newVal.cats = [] newVal.cats = [];
} }
if (newVal.target === '') { if (newVal.target === "") {
newVal.target = 'default' newVal.target = "default";
} }
if (initVal) { // patch if (initVal) {
updateSubscribe(groupNumber, newVal) // patch
.then(() => { updateSubscribe(groupNumber, newVal).then(() => {
setConfirmLoading(false); setConfirmLoading(false);
setShowModal(false); setShowModal(false);
form.resetFields(); form.resetFields();
refresh(); refresh();
}); });
} else { } else {
addSubscribe(groupNumber, newVal) addSubscribe(groupNumber, newVal).then(() => {
.then(() => {
setConfirmLoading(false); setConfirmLoading(false);
setShowModal(false); setShowModal(false);
form.resetFields(); form.resetFields();
refresh(); refresh();
}); });
} }
} };
const handleModleFinish = () => { const handleModleFinish = () => {
form.submit(); form.submit();
setConfirmLoading(() => true); setConfirmLoading(() => true);
} };
useEffect(() => { useEffect(() => {
if (initVal && !inited) { if (initVal && !inited) {
const platformName = initVal.platformName; const platformName = initVal.platformName;
setHasTarget(platformConf[platformName].hasTarget); setHasTarget(platformConf[platformName].hasTarget);
setCategories(platformConf[platformName].categories); setCategories(platformConf[platformName].categories);
setEnableTag(platformConf[platformName].enabledTag); setEnableTag(platformConf[platformName].enabledTag);
setInited(true) setInited(true);
form.setFieldsValue(initVal) form.setFieldsValue(initVal);
} }
}, [initVal, form, platformConf, inited]) }, [initVal, form, platformConf, inited]);
return <Modal title="添加订阅" visible={showModal} return (
confirmLoading={confirmLoading} onCancel={() => setShowModal(false)} <Modal
onOk={handleModleFinish}> title="添加订阅"
<Form form={form} labelCol={{ span: 6 }} name="b" onFinish={handleSubmit} > visible={showModal}
confirmLoading={confirmLoading}
onCancel={() => setShowModal(false)}
onOk={handleModleFinish}
>
<Form form={form} labelCol={{ span: 6 }} name="b" onFinish={handleSubmit}>
<Form.Item label="平台" name="platformName" rules={[]}> <Form.Item label="平台" name="platformName" rules={[]}>
<Select style={{ width: '80%' }} onChange={changePlatformSelect}> <Select style={{ width: "80%" }} onChange={changePlatformSelect}>
{Object.keys(platformConf).map(platformName => {Object.keys(platformConf).map((platformName) => (
<Select.Option key={platformName} value={platformName}>{platformConf[platformName].name}</Select.Option> <Select.Option key={platformName} value={platformName}>
)} {platformConf[platformName].name}
</Select.Option>
))}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item label="账号" name="target" rules={[ <Form.Item
{required: hasTarget, message: "请输入账号"}, label="账号"
{validator: async (_, value) => { name="target"
rules={[
{ required: hasTarget, message: "请输入账号" },
{
validator: async (_, value) => {
try { try {
const res = await getTargetName(form.getFieldValue('platformName'), value); const res = await getTargetName(
form.getFieldValue("platformName"),
value
);
if (res.targetName) { if (res.targetName) {
form.setFieldsValue({ form.setFieldsValue({
targetName: res.targetName targetName: res.targetName,
}) });
return Promise.resolve() return Promise.resolve();
} else { } else {
form.setFieldsValue({ form.setFieldsValue({
targetName: '' targetName: "",
}) });
return Promise.reject("账号不正确,请重新检查账号") return Promise.reject("账号不正确,请重新检查账号");
} }
} catch { } catch {
return Promise.reject('服务器错误,请稍后再试') return Promise.reject("服务器错误,请稍后再试");
} }
} },
} },
]}> ]}
<Input placeholder={hasTarget ? "获取方式见文档" : "此平台不需要账号"} >
disabled={! hasTarget} style={{ width: "80%" }}/> <Input
placeholder={hasTarget ? "获取方式见文档" : "此平台不需要账号"}
disabled={!hasTarget}
style={{ width: "80%" }}
/>
</Form.Item> </Form.Item>
<Form.Item label="账号名称" name="targetName"> <Form.Item label="账号名称" name="targetName">
<Input style={{ width: "80%" }} disabled /> <Input style={{ width: "80%" }} disabled />
</Form.Item> </Form.Item>
<Form.Item label="订阅分类" name="cats" rules={[ <Form.Item
{required: Object.keys(categories).length > 0, message: "请至少选择一个分类进行订阅"} label="订阅分类"
]}> name="cats"
<Select style={{ width: '80%' }} mode="multiple" rules={[
{
required: Object.keys(categories).length > 0,
message: "请至少选择一个分类进行订阅",
},
]}
>
<Select
style={{ width: "80%" }}
mode="multiple"
disabled={Object.keys(categories).length === 0} disabled={Object.keys(categories).length === 0}
placeholder={Object.keys(categories).length > 0 ? placeholder={
"请选择要订阅的分类" : "本平台不支持分类"}> Object.keys(categories).length > 0
? "请选择要订阅的分类"
: "本平台不支持分类"
}
>
{Object.keys(categories).length > 0 && {Object.keys(categories).length > 0 &&
Object.keys(categories).map((indexStr) => Object.keys(categories).map((indexStr) => (
<Select.Option key={indexStr} value={parseInt(indexStr)}> <Select.Option key={indexStr} value={parseInt(indexStr)}>
{categories[parseInt(indexStr)]} {categories[parseInt(indexStr)]}
</Select.Option> </Select.Option>
) ))}
}
</Select> </Select>
</Form.Item> </Form.Item>
<Form.Item label="订阅Tag" name="tags"> <Form.Item label="订阅Tag" name="tags">
<InputTagCustom disabled={!enabledTag}/> <InputTagCustom disabled={!enabledTag} />
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>
);
} }

View File

@ -1,31 +1,31 @@
import {Input, Tag, Tooltip} from "antd"; import { Input, Tag, Tooltip } from "antd";
import {PresetColorType, PresetStatusColorType} from 'antd/lib/_util/colors' import { PresetColorType, PresetStatusColorType } from "antd/lib/_util/colors";
import {LiteralUnion} from 'antd/lib/_util/type' import { LiteralUnion } from "antd/lib/_util/type";
import React, {useRef, useState, useEffect} from "react"; import React, { useRef, useState, useEffect } from "react";
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from "@ant-design/icons";
interface InputTagProp { interface InputTagProp {
value?: Array<string>, value?: Array<string>;
onChange?: (value: Array<string>) => void onChange?: (value: Array<string>) => void;
color?: LiteralUnion<PresetColorType | PresetStatusColorType, string>; color?: LiteralUnion<PresetColorType | PresetStatusColorType, string>;
addText?: string addText?: string;
} }
export function InputTag(prop: InputTagProp) { export function InputTag(prop: InputTagProp) {
const [ value, setValue ] = useState<Array<string>>(prop.value || []); const [value, setValue] = useState<Array<string>>(prop.value || []);
const [ inputVisible, setInputVisible ] = useState(false); const [inputVisible, setInputVisible] = useState(false);
const [ inputValue, setInputValue ] = useState(''); const [inputValue, setInputValue] = useState("");
const [ editInputIndex, setEditInputIndex ] = useState(-1); const [editInputIndex, setEditInputIndex] = useState(-1);
const [ editInputValue, setEditInputValue ] = useState(''); const [editInputValue, setEditInputValue] = useState("");
const inputRef = useRef(null as any); const inputRef = useRef(null as any);
const editInputRef = useRef(null as any); const editInputRef = useRef(null as any);
useEffect(() => { useEffect(() => {
if (prop.value) { if (prop.value) {
setValue(prop.value); setValue(prop.value);
} }
}, [prop.value]) }, [prop.value]);
useEffect(() => { useEffect(() => {
if (inputVisible) { if (inputVisible) {
inputRef.current.focus() inputRef.current.focus();
} }
}, [inputVisible]); }, [inputVisible]);
useEffect(() => { useEffect(() => {
@ -35,66 +35,80 @@ export function InputTag(prop: InputTagProp) {
}, [editInputIndex]); }, [editInputIndex]);
const handleClose = (removedTag: string) => { const handleClose = (removedTag: string) => {
const tags = value.filter(tag => tag !== removedTag); const tags = value.filter((tag) => tag !== removedTag);
setValue(_ => tags); setValue((_) => tags);
if (prop.onChange) { if (prop.onChange) {
prop.onChange(tags); prop.onChange(tags);
} }
} };
const showInput = () => { const showInput = () => {
setInputVisible(_ => true); setInputVisible((_) => true);
} };
const handleInputConfirm = () => { const handleInputConfirm = () => {
if (inputValue && value.indexOf(inputValue) === -1) { if (inputValue && value.indexOf(inputValue) === -1) {
const newVal = [...value, inputValue]; const newVal = [...value, inputValue];
setValue(_ => newVal); setValue((_) => newVal);
if (prop.onChange) { if (prop.onChange) {
prop.onChange(newVal); prop.onChange(newVal);
} }
} }
setInputVisible(_ => false); setInputVisible((_) => false);
setInputValue(_ => ''); setInputValue((_) => "");
} };
const handleEditInputChange = (e: any) => { const handleEditInputChange = (e: any) => {
setEditInputValue(_ => e.target.value); setEditInputValue((_) => e.target.value);
} };
const handleEditInputConfirm = () => { const handleEditInputConfirm = () => {
const newTags = value.slice(); const newTags = value.slice();
newTags[editInputIndex] = editInputValue; newTags[editInputIndex] = editInputValue;
setValue(_ => newTags); setValue((_) => newTags);
if (prop.onChange) { if (prop.onChange) {
prop.onChange(newTags); prop.onChange(newTags);
} }
setEditInputIndex(_ => -1); setEditInputIndex((_) => -1);
setEditInputValue(_ => ''); setEditInputValue((_) => "");
} };
const handleInputChange = (e: any) => { const handleInputChange = (e: any) => {
setInputValue(e.target.value); setInputValue(e.target.value);
} };
return ( return (
<> <>
{ value.map((tag, index) => { {value.map((tag, index) => {
if (editInputIndex === index) { if (editInputIndex === index) {
return ( return (
<Input ref={editInputRef} key={tag} size="small" <Input
value={editInputValue} onChange={handleEditInputChange} ref={editInputRef}
onBlur={handleEditInputConfirm} onPressEnter={handleInputConfirm} /> key={tag}
size="small"
value={editInputValue}
onChange={handleEditInputChange}
onBlur={handleEditInputConfirm}
onPressEnter={handleInputConfirm}
/>
); );
} }
const isLongTag = tag.length > 20; const isLongTag = tag.length > 20;
const tagElem = ( const tagElem = (
<Tag color={prop.color || "default"} style={{userSelect: 'none'}} key={tag} closable onClose={() => handleClose(tag)}> <Tag
<span onDoubleClick={e => { color={prop.color || "default"}
setEditInputIndex(_ => index); style={{ userSelect: "none" }}
setEditInputValue(_ => tag); key={tag}
closable
onClose={() => handleClose(tag)}
>
<span
onDoubleClick={(e) => {
setEditInputIndex((_) => index);
setEditInputValue((_) => tag);
e.preventDefault(); e.preventDefault();
}}> }}
>
{isLongTag ? `${tag.slice(0, 20)}...` : tag} {isLongTag ? `${tag.slice(0, 20)}...` : tag}
</span> </span>
</Tag> </Tag>
@ -103,20 +117,35 @@ export function InputTag(prop: InputTagProp) {
<Tooltip title={tag} key={tag}> <Tooltip title={tag} key={tag}>
{tagElem} {tagElem}
</Tooltip> </Tooltip>
) : ( tagElem ); ) : (
tagElem
);
})} })}
{inputVisible && ( {inputVisible && (
<Input ref={inputRef} type="text" size="small" <Input
style={{width: '78px', marginRight: '8px', verticalAlign: 'top'}} value={inputValue} ref={inputRef}
onChange={handleInputChange} onBlur={handleInputConfirm} type="text"
onPressEnter={handleInputConfirm} /> size="small"
style={{ width: "78px", marginRight: "8px", verticalAlign: "top" }}
value={inputValue}
onChange={handleInputChange}
onBlur={handleInputConfirm}
onPressEnter={handleInputConfirm}
/>
)} )}
{!inputVisible && ( {!inputVisible && (
<Tag className="site-tag-plus" onClick={showInput} style={{background: '#fff', border: 'dashed thin', borderColor: '#bfbfbf' }}> <Tag
<PlusOutlined/> {prop.addText || "Add Tag"} className="site-tag-plus"
onClick={showInput}
style={{
background: "#fff",
border: "dashed thin",
borderColor: "#bfbfbf",
}}
>
<PlusOutlined /> {prop.addText || "Add Tag"}
</Tag> </Tag>
)} )}
</> </>
); );
} }

View File

@ -1,108 +1,190 @@
import {CopyOutlined, DeleteOutlined, EditOutlined} from '@ant-design/icons'; import { CopyOutlined, DeleteOutlined, EditOutlined } from "@ant-design/icons";
import {Card, Col, Form, message, Popconfirm, Select, Tag, Tooltip} from 'antd'; import {
import Modal from 'antd/lib/modal/Modal'; Card,
import React, {useState} from "react"; Col,
import {useDispatch, useSelector} from 'react-redux'; Form,
import {addSubscribe, delSubscribe} from 'src/api/config'; message,
import {platformConfSelector} from 'src/store/globalConfSlice'; Popconfirm,
import {groupConfigSelector, updateGroupSubs} from 'src/store/groupConfigSlice'; Select,
import {PlatformConfig, SubscribeConfig, SubscribeResp} from 'src/utils/type'; Tag,
import {AddModal} from './addSubsModal'; Tooltip,
} from "antd";
import Modal from "antd/lib/modal/Modal";
import React, { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { addSubscribe, delSubscribe } from "src/api/config";
import { platformConfSelector } from "src/store/globalConfSlice";
import {
groupConfigSelector,
updateGroupSubs,
} from "src/store/groupConfigSlice";
import { PlatformConfig, SubscribeConfig, SubscribeResp } from "src/utils/type";
import { AddModal } from "./addSubsModal";
interface CopyModalProp { interface CopyModalProp {
setShowModal: (modalShow: boolean) => void setShowModal: (modalShow: boolean) => void;
showModal: boolean showModal: boolean;
config: SubscribeConfig, config: SubscribeConfig;
groups: SubscribeResp groups: SubscribeResp;
currentGroupNumber: string currentGroupNumber: string;
reload: () => void reload: () => void;
} }
function CopyModal({setShowModal,config, function CopyModal({
currentGroupNumber,groups,showModal,reload}: CopyModalProp) { setShowModal,
const [confirmLoading, setConfirmLoading] = useState(false) config,
const [ selectedGroups, setSelectGroups ] = useState<Array<string>>([]); currentGroupNumber,
const postReqs = async (selectedGroups: Array<string>, config: SubscribeConfig) => { groups,
for(let selectedGroup of selectedGroups) { showModal,
reload,
}: CopyModalProp) {
const [confirmLoading, setConfirmLoading] = useState(false);
const [selectedGroups, setSelectGroups] = useState<Array<string>>([]);
const postReqs = async (
selectedGroups: Array<string>,
config: SubscribeConfig
) => {
for (let selectedGroup of selectedGroups) {
await addSubscribe(selectedGroup, config); await addSubscribe(selectedGroup, config);
} }
} };
const handleOk = () => { const handleOk = () => {
if (selectedGroups.length === 0) { if (selectedGroups.length === 0) {
message.error("请至少选择一个目标群"); message.error("请至少选择一个目标群");
} else{ } else {
setConfirmLoading(true) setConfirmLoading(true);
postReqs(selectedGroups, config).then(() => { postReqs(selectedGroups, config).then(() => {
setConfirmLoading(false) setConfirmLoading(false);
setShowModal(false) setShowModal(false);
return reload() return reload();
}) });
} }
} };
return <Modal title="复制订阅" visible={showModal} confirmLoading={confirmLoading} return (
onCancel={() => setShowModal(false)} onOk={handleOk}> <Modal
<Select mode="multiple" onChange={(value: Array<string>) => setSelectGroups(value)} title="复制订阅"
style={{width: '80%'}}> visible={showModal}
{ confirmLoading={confirmLoading}
Object.keys(groups).filter(groupNumber => groupNumber !== currentGroupNumber) onCancel={() => setShowModal(false)}
.map((groupNumber) => onOk={handleOk}
>
<Select
mode="multiple"
onChange={(value: Array<string>) => setSelectGroups(value)}
style={{ width: "80%" }}
>
{Object.keys(groups)
.filter((groupNumber) => groupNumber !== currentGroupNumber)
.map((groupNumber) => (
<Select.Option value={groupNumber} key={groupNumber}> <Select.Option value={groupNumber} key={groupNumber}>
{`${groupNumber} - ${groups[groupNumber].name}`} {`${groupNumber} - ${groups[groupNumber].name}`}
</Select.Option>) </Select.Option>
} ))}
</Select> </Select>
</Modal> </Modal>
);
} }
interface SubscribeCardProp { interface SubscribeCardProp {
groupNumber: string groupNumber: string;
config: SubscribeConfig config: SubscribeConfig;
} }
export function SubscribeCard({groupNumber, config}: SubscribeCardProp) { export function SubscribeCard({ groupNumber, config }: SubscribeCardProp) {
const platformConfs = useSelector(platformConfSelector) const platformConfs = useSelector(platformConfSelector);
const [showModal, setShowModal] = useState(false) const [showModal, setShowModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false) const [showEditModal, setShowEditModal] = useState(false);
const platformConf = platformConfs[config.platformName] as PlatformConfig; const platformConf = platformConfs[config.platformName] as PlatformConfig;
const dispatcher = useDispatch(); const dispatcher = useDispatch();
const groupSubscribes = useSelector(groupConfigSelector); const groupSubscribes = useSelector(groupConfigSelector);
const reload = () => dispatcher(updateGroupSubs()) const reload = () => dispatcher(updateGroupSubs());
const handleDelete = (groupNumber: string, platformName: string, target: string) => () => { const handleDelete =
(groupNumber: string, platformName: string, target: string) => () => {
delSubscribe(groupNumber, platformName, target).then(() => { delSubscribe(groupNumber, platformName, target).then(() => {
reload() reload();
}) });
} };
return ( return (
<Col span={8} key={`${config.platformName}-${config.target}`}> <Col span={8} key={`${config.platformName}-${config.target}`}>
<Card title={`${platformConf.name} - ${config.targetName}`} <Card
title={`${platformConf.name} - ${config.targetName}`}
actions={[ actions={[
<Tooltip title="编辑"> <Tooltip title="编辑">
<EditOutlined onClick={()=>{setShowEditModal(state => !state)}}/> <EditOutlined
onClick={() => {
setShowEditModal((state) => !state);
}}
/>
</Tooltip>, </Tooltip>,
<Tooltip title="添加到其他群"> <Tooltip title="添加到其他群">
<CopyOutlined onClick={()=>{setShowModal(state => !state)}}/> <CopyOutlined
onClick={() => {
setShowModal((state) => !state);
}}
/>
</Tooltip>, </Tooltip>,
<Popconfirm title={`确定要删除 ${platformConf.name} - ${config.targetName}`} <Popconfirm
onConfirm={handleDelete(groupNumber, config.platformName, config.target || 'default')}> title={`确定要删除 ${platformConf.name} - ${config.targetName}`}
<Tooltip title="删除" ><DeleteOutlined /></Tooltip> onConfirm={handleDelete(
groupNumber,
config.platformName,
config.target || "default"
)}
>
<Tooltip title="删除">
<DeleteOutlined />
</Tooltip>
</Popconfirm>, </Popconfirm>,
]}> ]}
>
<Form labelCol={{ span: 4 }}> <Form labelCol={{ span: 4 }}>
<Form.Item label="订阅帐号"> <Form.Item label="订阅帐号">
{ platformConf.hasTarget ? config.target : <Tag color="default"></Tag> } {platformConf.hasTarget ? (
config.target
) : (
<Tag color="default"></Tag>
)}
</Form.Item> </Form.Item>
<Form.Item label="订阅类型"> <Form.Item label="订阅类型">
{Object.keys(platformConf.categories).length > 0 ? {Object.keys(platformConf.categories).length > 0 ? (
config.cats.map((catKey: number) => (<Tag color="green" key={catKey}>{platformConf.categories[catKey]}</Tag>)) : config.cats.map((catKey: number) => (
<Tag color="default"></Tag>} <Tag color="green" key={catKey}>
{platformConf.categories[catKey]}
</Tag>
))
) : (
<Tag color="default"></Tag>
)}
</Form.Item> </Form.Item>
<Form.Item label="订阅Tag"> <Form.Item label="订阅Tag">
{platformConf.enabledTag ? config.tags.length > 0 ? config.tags.map(tag => (<Tag color="green" key={tag}>{tag}</Tag>)) : (<Tag color="blue"></Tag>) : {platformConf.enabledTag ? (
<Tag color="default">Tag</Tag>} config.tags.length > 0 ? (
config.tags.map((tag) => (
<Tag color="green" key={tag}>
{tag}
</Tag>
))
) : (
<Tag color="blue"></Tag>
)
) : (
<Tag color="default">Tag</Tag>
)}
</Form.Item> </Form.Item>
</Form> </Form>
</Card> </Card>
<CopyModal setShowModal={setShowModal} reload={reload} currentGroupNumber={groupNumber} <CopyModal
showModal={showModal} config={config} groups={groupSubscribes}/> setShowModal={setShowModal}
<AddModal showModal={showEditModal} setShowModal={setShowEditModal} reload={reload}
groupNumber={groupNumber} refresh={reload} initVal={config}/> currentGroupNumber={groupNumber}
showModal={showModal}
config={config}
groups={groupSubscribes}
/>
<AddModal
showModal={showEditModal}
setShowModal={setShowEditModal}
groupNumber={groupNumber}
refresh={reload}
initVal={config}
/>
</Col> </Col>
) );
} }

View File

@ -1,11 +1,11 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom'; import ReactDOM from "react-dom";
import {Provider} from 'react-redux'; import { Provider } from "react-redux";
import App from './App'; import App from "./App";
import './index.css'; import "./index.css";
import reportWebVitals from './reportWebVitals'; import reportWebVitals from "./reportWebVitals";
import store from './store'; import store from "./store";
import {injectStore} from 'src/api/utils'; import { injectStore } from "src/api/utils";
injectStore(store); injectStore(store);
ReactDOM.render( ReactDOM.render(
@ -14,7 +14,7 @@ ReactDOM.render(
<App /> <App />
</Provider> </Provider>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById("root")
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

View File

@ -1,56 +1,73 @@
import {Button, Collapse, Empty, Row} from 'antd'; import { Button, Collapse, Empty, Row } from "antd";
import React, {ReactElement, useEffect, useState} from "react"; import React, { ReactElement, useEffect, useState } from "react";
import {useDispatch, useSelector} from 'react-redux'; import { useDispatch, useSelector } from "react-redux";
import {AddModal} from 'src/component/addSubsModal'; import { AddModal } from "src/component/addSubsModal";
import {SubscribeCard} from 'src/component/subscribeCard'; import { SubscribeCard } from "src/component/subscribeCard";
import {groupConfigSelector, updateGroupSubs} from 'src/store/groupConfigSlice'; import {
groupConfigSelector,
updateGroupSubs,
} from "src/store/groupConfigSlice";
interface ConfigPageProp { interface ConfigPageProp {
tab: string tab: string;
} }
export function ConfigPage(prop: ConfigPageProp) { export function ConfigPage(prop: ConfigPageProp) {
const [ showModal, setShowModal ] = useState<boolean>(false); const [showModal, setShowModal] = useState<boolean>(false);
const [ currentAddingGroupNumber, setCurrentAddingGroupNumber ] = useState(''); const [currentAddingGroupNumber, setCurrentAddingGroupNumber] = useState("");
const configData = useSelector(groupConfigSelector); const configData = useSelector(groupConfigSelector);
const dispatcher = useDispatch(); const dispatcher = useDispatch();
useEffect(() => { useEffect(() => {
dispatcher(updateGroupSubs()) dispatcher(updateGroupSubs());
}, [prop.tab, dispatcher]); }, [prop.tab, dispatcher]);
const clickNew = (groupNumber: string) => (e: React.MouseEvent<HTMLButtonElement>) => { const clickNew =
setShowModal(_ => true); (groupNumber: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
setShowModal((_) => true);
setCurrentAddingGroupNumber(groupNumber); setCurrentAddingGroupNumber(groupNumber);
e.stopPropagation(); e.stopPropagation();
} };
if (Object.keys(configData).length === 0) { if (Object.keys(configData).length === 0) {
return <Empty /> return <Empty />;
} else { } else {
let groups: Array<ReactElement> = []; let groups: Array<ReactElement> = [];
for (let key of Object.keys(configData)) { for (let key of Object.keys(configData)) {
let value = configData[key]; let value = configData[key];
groups.push( groups.push(
<Collapse.Panel key={key} header={ <Collapse.Panel
<span>{`${key} - ${value.name}`}<Button style={{float: "right"}} onClick={clickNew(key)}></Button></span> key={key}
}> header={
<Row gutter={[{ xs: 8, sm: 16, md: 24, lg: 32}, <span>
{ xs: 8, sm: 16, md: 24, lg: 32}]} align="middle"> {`${key} - ${value.name}`}
{value.subscribes.map((subs, idx) => <SubscribeCard key={idx} <Button style={{ float: "right" }} onClick={clickNew(key)}>
groupNumber={key} config={subs} />)}
</Button>
</span>
}
>
<Row
gutter={[
{ xs: 8, sm: 16, md: 24, lg: 32 },
{ xs: 8, sm: 16, md: 24, lg: 32 },
]}
align="middle"
>
{value.subscribes.map((subs, idx) => (
<SubscribeCard key={idx} groupNumber={key} config={subs} />
))}
</Row> </Row>
</Collapse.Panel> </Collapse.Panel>
) );
} }
return ( return (
<div> <div>
<Collapse> <Collapse>{groups}</Collapse>
{groups} <AddModal
</Collapse> groupNumber={currentAddingGroupNumber}
<AddModal groupNumber={currentAddingGroupNumber} showModal={showModal} showModal={showModal}
refresh={() => dispatcher(updateGroupSubs())} refresh={() => dispatcher(updateGroupSubs())}
setShowModal={(s: boolean) => setShowModal(_ => s)} /> setShowModal={(s: boolean) => setShowModal((_) => s)}
/>
</div> </div>
) );
} }
} }

View File

@ -1,37 +1,39 @@
import {BugOutlined, SettingOutlined} from '@ant-design/icons'; import { BugOutlined, SettingOutlined } from "@ant-design/icons";
import {Layout, Menu} from 'antd'; import { Layout, Menu } from "antd";
import React, {useState} from "react"; import React, { useState } from "react";
import {useSelector} from 'react-redux'; import { useSelector } from "react-redux";
import {loginSelector} from 'src/store/loginSlice'; import { loginSelector } from "src/store/loginSlice";
import './admin.css'; import "./admin.css";
import {ConfigPage} from './configPage'; import { ConfigPage } from "./configPage";
export function Admin() { export function Admin() {
const login = useSelector(loginSelector) const login = useSelector(loginSelector);
const [ tab, changeTab ] = useState("manage"); const [tab, changeTab] = useState("manage");
return ( return (
<Layout style={{ minHeight: '100vh' }}> <Layout style={{ minHeight: "100vh" }}>
<Layout.Sider className="layout-side"> <Layout.Sider className="layout-side">
<div className="user"> <div className="user"></div>
</div> <Menu
<Menu mode="inline" theme="dark" defaultSelectedKeys={[tab]} mode="inline"
onClick={({key}) => changeTab(key)}> theme="dark"
<Menu.Item key="manage" icon={<SettingOutlined />}></Menu.Item> defaultSelectedKeys={[tab]}
{ login.type === 'admin' && onClick={({ key }) => changeTab(key)}
<Menu.Item key="log" icon={<BugOutlined />}></Menu.Item> >
} <Menu.Item key="manage" icon={<SettingOutlined />}>
</Menu.Item>
{login.type === "admin" && (
<Menu.Item key="log" icon={<BugOutlined />}>
</Menu.Item>
)}
</Menu> </Menu>
</Layout.Sider> </Layout.Sider>
<Layout.Content> <Layout.Content>
<div style={{margin: '24px', background: '#fff', minHeight: '640px'}}> <div style={{ margin: "24px", background: "#fff", minHeight: "640px" }}>
{ {tab === "manage" ? <ConfigPage tab={tab} /> : null}
tab === 'manage' ?
<ConfigPage tab={tab}/>
: null
}
</div> </div>
</Layout.Content> </Layout.Content>
</Layout> </Layout>
) );
} }

View File

@ -1,28 +1,31 @@
import React, {useEffect} from "react"; import React, { useEffect } from "react";
import {useDispatch, useSelector} from "react-redux"; import { useDispatch, useSelector } from "react-redux";
import {useParams} from "react-router"; import { useParams } from "react-router";
import {Redirect} from 'react-router-dom'; import { Redirect } from "react-router-dom";
import {login, loginSelector} from 'src/store/loginSlice'; import { login, loginSelector } from "src/store/loginSlice";
interface AuthParam { interface AuthParam {
code: string code: string;
} }
export function Auth() { export function Auth() {
const { code } = useParams<AuthParam>(); const { code } = useParams<AuthParam>();
const dispatch = useDispatch(); const dispatch = useDispatch();
const loginState = useSelector(loginSelector) const loginState = useSelector(loginSelector);
useEffect(() => { useEffect(() => {
const loginFun = async () => { const loginFun = async () => {
dispatch(login(code)); dispatch(login(code));
} };
loginFun(); loginFun();
}, [code, dispatch]) }, [code, dispatch]);
return <> return (
{ loginState.login ? <>
<Redirect to={{pathname: '/admin'}} /> : {loginState.login ? (
loginState.failed ? <Redirect to={{ pathname: "/admin" }} />
<div></div> : ) : loginState.failed ? (
<div></div>
) : (
<div>Logining...</div> <div>Logining...</div>
} )}
</>; </>
);
} }

View File

@ -1,8 +1,8 @@
import { ReportHandler } from 'web-vitals'; import { ReportHandler } from "web-vitals";
const reportWebVitals = (onPerfEntry?: ReportHandler) => { const reportWebVitals = (onPerfEntry?: ReportHandler) => {
if (onPerfEntry && onPerfEntry instanceof Function) { if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry); getCLS(onPerfEntry);
getFID(onPerfEntry); getFID(onPerfEntry);
getFCP(onPerfEntry); getFCP(onPerfEntry);

View File

@ -2,4 +2,4 @@
// allows you to do things like: // allows you to do things like:
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom'; import "@testing-library/jest-dom";

View File

@ -1,23 +1,31 @@
import {CaseReducer, createAsyncThunk, createSlice, PayloadAction} from "@reduxjs/toolkit"; import {
import {getGlobalConf as getGlobalConfApi} from "src/api/config"; CaseReducer,
import {GlobalConf} from "src/utils/type"; createAsyncThunk,
import {RootState} from "."; createSlice,
PayloadAction,
} from "@reduxjs/toolkit";
import { getGlobalConf as getGlobalConfApi } from "src/api/config";
import { GlobalConf } from "src/utils/type";
import { RootState } from ".";
const initialState: GlobalConf = { const initialState: GlobalConf = {
platformConf: {}, platformConf: {},
loaded: false loaded: false,
} };
const setGlobalConf: CaseReducer<GlobalConf, PayloadAction<GlobalConf>> = (_, action) => { const setGlobalConf: CaseReducer<GlobalConf, PayloadAction<GlobalConf>> = (
return {...action.payload, loaded: true} _,
} action
) => {
return { ...action.payload, loaded: true };
};
export const getGlobalConf = createAsyncThunk( export const getGlobalConf = createAsyncThunk(
"globalConf/set", "globalConf/set",
getGlobalConfApi, getGlobalConfApi,
{ {
condition: (_, { getState }) => !(getState() as RootState).globalConf.loaded condition: (_, { getState }) =>
!(getState() as RootState).globalConf.loaded,
} }
); );
@ -26,10 +34,11 @@ export const globalConfSlice = createSlice({
initialState, initialState,
reducers: {}, reducers: {},
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(getGlobalConf.fulfilled, setGlobalConf) builder.addCase(getGlobalConf.fulfilled, setGlobalConf);
} },
}) });
export const platformConfSelector = (state: RootState) => state.globalConf.platformConf export const platformConfSelector = (state: RootState) =>
state.globalConf.platformConf;
export default globalConfSlice.reducer export default globalConfSlice.reducer;

View File

@ -1,27 +1,36 @@
import {CaseReducer, createAsyncThunk, createSlice, PayloadAction} from '@reduxjs/toolkit'; import {
import {SubscribeResp} from 'src/utils/type'; CaseReducer,
import {getSubscribe} from 'src/api/config'; createAsyncThunk,
import {RootState} from '.'; createSlice,
const initialState: SubscribeResp = {} PayloadAction,
} from "@reduxjs/toolkit";
import { SubscribeResp } from "src/utils/type";
import { getSubscribe } from "src/api/config";
import { RootState } from ".";
const initialState: SubscribeResp = {};
const setSubs: CaseReducer<SubscribeResp, PayloadAction<SubscribeResp>> = (_, action) => { const setSubs: CaseReducer<SubscribeResp, PayloadAction<SubscribeResp>> = (
return action.payload _,
} action
) => {
return action.payload;
};
export const updateGroupSubs = createAsyncThunk( export const updateGroupSubs = createAsyncThunk(
"groupConfig/update", getSubscribe "groupConfig/update",
) getSubscribe
);
export const groupConfigSlice = createSlice({ export const groupConfigSlice = createSlice({
name: "groupConfig", name: "groupConfig",
initialState, initialState,
reducers: { reducers: {
setSubs setSubs,
}, },
extraReducers: (reducer) => { extraReducers: (reducer) => {
reducer.addCase(updateGroupSubs.fulfilled, setSubs) reducer.addCase(updateGroupSubs.fulfilled, setSubs);
} },
}) });
export const groupConfigSelector = (state: RootState) => state.groupConfig; export const groupConfigSelector = (state: RootState) => state.groupConfig;
export default groupConfigSlice.reducer; export default groupConfigSlice.reducer;

View File

@ -1,5 +1,5 @@
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux"; import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import {AppDispatch, RootState} from "."; import { AppDispatch, RootState } from ".";
export const useAppDispacher = () => useDispatch<AppDispatch>() export const useAppDispacher = () => useDispatch<AppDispatch>();
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;

View File

@ -1,15 +1,15 @@
import {configureStore} from "@reduxjs/toolkit"; import { configureStore } from "@reduxjs/toolkit";
import loginSlice from "./loginSlice"; import loginSlice from "./loginSlice";
import globalConfSlice from "./globalConfSlice"; import globalConfSlice from "./globalConfSlice";
import groupConfigSlice from './groupConfigSlice'; import groupConfigSlice from "./groupConfigSlice";
const store = configureStore({ const store = configureStore({
reducer: { reducer: {
login: loginSlice, login: loginSlice,
globalConf: globalConfSlice, globalConf: globalConfSlice,
groupConfig: groupConfigSlice, groupConfig: groupConfigSlice,
} },
}) });
export default store; export default store;

View File

@ -1,108 +1,121 @@
import { AnyAction, CaseReducer, createAsyncThunk, createSlice, PayloadAction, ThunkAction } from "@reduxjs/toolkit"; import {
import jwt_decode from 'jwt-decode'; AnyAction,
CaseReducer,
createAsyncThunk,
createSlice,
PayloadAction,
ThunkAction,
} from "@reduxjs/toolkit";
import jwt_decode from "jwt-decode";
import { LoginStatus, TokenResp } from "src/utils/type"; import { LoginStatus, TokenResp } from "src/utils/type";
import { auth } from "src/api/config"; import { auth } from "src/api/config";
import {RootState} from "."; import { RootState } from ".";
const initialState: LoginStatus = { const initialState: LoginStatus = {
login: false, login: false,
type: '', type: "",
name: '', name: "",
id: '123', id: "123",
// groups: [], // groups: [],
token: '', token: "",
failed: false failed: false,
} };
interface storedInfo { interface storedInfo {
type: string type: string;
name: string name: string;
id: string id: string;
} }
const loginAction: CaseReducer<LoginStatus, PayloadAction<TokenResp>> = (_, action) => { const loginAction: CaseReducer<LoginStatus, PayloadAction<TokenResp>> = (
_,
action
) => {
return { return {
login: true, login: true,
failed: false, failed: false,
type: action.payload.type, type: action.payload.type,
name: action.payload.name, name: action.payload.name,
id: action.payload.id, id: action.payload.id,
token: action.payload.token token: action.payload.token,
} };
} };
export const login = createAsyncThunk( export const login = createAsyncThunk(
"auth/login", "auth/login",
async (code: string) => { async (code: string) => {
let res = await auth(code); let res = await auth(code);
if (res.status !== 200) { if (res.status !== 200) {
throw Error("Login Error") throw Error("Login Error");
} else { } else {
localStorage.setItem('loginInfo', JSON.stringify({ localStorage.setItem(
'type': res.type, "loginInfo",
'name': res.name, JSON.stringify({
type: res.type,
name: res.name,
id: res.id, id: res.id,
})) })
localStorage.setItem('token', res.token) );
localStorage.setItem("token", res.token);
} }
return res return res;
}, },
{ {
condition: (_: string, { getState }) => { condition: (_: string, { getState }) => {
const { login } = getState() as { login: LoginStatus } const { login } = getState() as { login: LoginStatus };
return !login.login; return !login.login;
},
} }
} );
)
export const loginSlice = createSlice({ export const loginSlice = createSlice({
name: 'auth', name: "auth",
initialState, initialState,
reducers: { reducers: {
doLogin: loginAction, doLogin: loginAction,
doClearLogin: (state) => { doClearLogin: (state) => {
state.login = false state.login = false;
} },
}, },
extraReducers: (builder) => { extraReducers: (builder) => {
builder.addCase(login.fulfilled, loginAction); builder.addCase(login.fulfilled, loginAction);
builder.addCase(login.rejected, (stat) => { builder.addCase(login.rejected, (stat) => {
stat.failed = true stat.failed = true;
}) });
} },
}) });
export const { doLogin, doClearLogin } = loginSlice.actions export const { doLogin, doClearLogin } = loginSlice.actions;
export const loadLoginState = (): ThunkAction<void, RootState, unknown, AnyAction> => export const loadLoginState =
(): ThunkAction<void, RootState, unknown, AnyAction> =>
(dispatch, getState) => { (dispatch, getState) => {
if (getState().login.login) { if (getState().login.login) {
return return;
} }
const infoJson = localStorage.getItem('loginInfo') const infoJson = localStorage.getItem("loginInfo");
const jwtToken = localStorage.getItem('token'); const jwtToken = localStorage.getItem("token");
if (infoJson && jwtToken) { if (infoJson && jwtToken) {
const decodedJwt = jwt_decode(jwtToken) as { exp: number }; const decodedJwt = jwt_decode(jwtToken) as { exp: number };
if (decodedJwt.exp < Date.now() / 1000) { if (decodedJwt.exp < Date.now() / 1000) {
return return;
} }
const info = JSON.parse(infoJson) as storedInfo const info = JSON.parse(infoJson) as storedInfo;
const payload: TokenResp = { const payload: TokenResp = {
...info, ...info,
status: 200, status: 200,
token: jwtToken, token: jwtToken,
};
dispatch(doLogin(payload));
} }
dispatch(doLogin(payload)) };
}
}
export const clearLoginStatus = (): ThunkAction<void, RootState, unknown, AnyAction> => export const clearLoginStatus =
(dispatch) => { (): ThunkAction<void, RootState, unknown, AnyAction> => (dispatch) => {
localStorage.removeItem('loginInfo') localStorage.removeItem("loginInfo");
localStorage.removeItem('token') localStorage.removeItem("token");
dispatch(doClearLogin()) dispatch(doClearLogin());
} };
export const loginSelector = (state: RootState) => state.login export const loginSelector = (state: RootState) => state.login;
export default loginSlice.reducer export default loginSlice.reducer;

View File

@ -1,34 +1,34 @@
interface QQGroup { interface QQGroup {
id: string, id: string;
name: string, name: string;
} }
export interface LoginStatus { export interface LoginStatus {
login: boolean login: boolean;
type: string type: string;
name: string name: string;
id: string id: string;
// groups: Array<QQGroup> // groups: Array<QQGroup>
token: string, token: string;
failed: boolean, failed: boolean;
} }
export type LoginContextType = { export type LoginContextType = {
login: LoginStatus login: LoginStatus;
save: (status: LoginStatus) => void save: (status: LoginStatus) => void;
} };
export interface SubscribeConfig { export interface SubscribeConfig {
platformName: string platformName: string;
target: string target: string;
targetName: string targetName: string;
cats: Array<number> cats: Array<number>;
tags: Array<string> tags: Array<string>;
} }
export interface GlobalConf { export interface GlobalConf {
platformConf: AllPlatformConf, platformConf: AllPlatformConf;
loaded: boolean loaded: boolean;
} }
export interface AllPlatformConf { export interface AllPlatformConf {
@ -36,34 +36,34 @@ export interface AllPlatformConf {
} }
export interface CategoryConfig { export interface CategoryConfig {
[idx: number]: string [idx: number]: string;
} }
export interface PlatformConfig { export interface PlatformConfig {
name: string name: string;
categories: CategoryConfig categories: CategoryConfig;
enabledTag: boolean, enabledTag: boolean;
platformName: string, platformName: string;
hasTarget: boolean hasTarget: boolean;
} }
export interface TokenResp { export interface TokenResp {
status: number, status: number;
token: string, token: string;
type: string, type: string;
id: string id: string;
name: string name: string;
} }
export interface SubscribeGroupDetail { export interface SubscribeGroupDetail {
name: string, name: string;
subscribes: Array<SubscribeConfig> subscribes: Array<SubscribeConfig>;
} }
export interface SubscribeResp { export interface SubscribeResp {
[idx: string]: SubscribeGroupDetail [idx: string]: SubscribeGroupDetail;
} }
export interface TargetNameResp { export interface TargetNameResp {
targetName: string targetName: string;
} }

View File

@ -9,9 +9,9 @@ RUN apt-get update && apt-get install -y xvfb fonts-noto-color-emoji ttf-unifont
libnspr4 libnss3 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ libnspr4 libnss3 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
libxdamage1 libxext6 libxfixes3 libxrandr2 libxshmfence1 \ libxdamage1 libxext6 libxfixes3 libxrandr2 libxshmfence1 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN pip install playwright && playwright install chromium
COPY ./pyproject.toml ./poetry.lock* /app/ COPY ./pyproject.toml ./poetry.lock* /app/
RUN poetry install --no-root --no-dev RUN poetry install --no-root --no-dev
RUN playwright install chromium
ADD src /app/src ADD src /app/src
ADD bot.py /app/ ADD bot.py /app/
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0

View File

@ -9,11 +9,11 @@ RUN apt-get update && apt-get install -y xvfb fonts-noto-color-emoji ttf-unifont
libnspr4 libnss3 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \ libnspr4 libnss3 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 \
libxdamage1 libxext6 libxfixes3 libxrandr2 libxshmfence1 \ libxdamage1 libxext6 libxfixes3 libxrandr2 libxshmfence1 \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
RUN pip install playwright && playwright install chromium
COPY ./pyproject.toml ./poetry.lock* ./bot.py /app/ COPY ./pyproject.toml ./poetry.lock* ./bot.py /app/
RUN poetry add nonebot-plugin-sentry && \ RUN poetry add nonebot-plugin-sentry && \
sed '/nonebot.load_builtin_plugins()/a nonebot.load_plugin("nonebot_plugin_sentry")' -i bot.py sed '/nonebot.load_builtin_plugins()/a nonebot.load_plugin("nonebot_plugin_sentry")' -i bot.py
RUN poetry install --no-root --no-dev RUN poetry install --no-root --no-dev
RUN playwright install chromium
ADD src /app/src ADD src /app/src
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
CMD ["python", "bot.py"] CMD ["python", "bot.py"]

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "nonebot-bison" name = "nonebot-bison"
version = "0.4.3" version = "0.4.4"
description = "Subscribe message from social medias" description = "Subscribe message from social medias"
authors = ["felinae98 <felinae225@qq.com>"] authors = ["felinae98 <felinae225@qq.com>"]
license = "MIT" license = "MIT"
@ -61,16 +61,3 @@ markers = [
"render: render img by chrome" "render: render img by chrome"
] ]
[tool.black]
line-length = 88
target-version = ["py39", "py310"]
include = '\.pyi?$'
extend-exclude = '''
'''
[tool.isort]
profile = "black"
line_length = 88
length_sort = true
skip_gitignore = true
force_sort_within_sections = true

View File

@ -1,9 +1,9 @@
from typing import Union from typing import Union
from nonebot import on_request from nonebot import on_request
from nonebot.log import logger
from nonebot.adapters.onebot.v11 import Bot from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.event import GroupRequestEvent, FriendRequestEvent from nonebot.adapters.onebot.v11.event import FriendRequestEvent, GroupRequestEvent
from nonebot.log import logger
friend_req = on_request(priority=5) friend_req = on_request(priority=5)

View File

@ -1,18 +1,15 @@
import nonebot
from . import ( from . import (
admin_page,
config,
config_manager,
platform,
post, post,
scheduler,
send, send,
types, types,
utils, utils,
config,
platform,
scheduler,
admin_page,
config_manager,
) )
__help__version__ = "0.4.3" __help__version__ = "0.4.3"
__help__plugin__name__ = "nonebot_bison" __help__plugin__name__ = "nonebot_bison"
__usage__ = "本bot可以提供b站、微博等社交媒体的消息订阅详情" "请查看本bot文档或者at本bot发送“添加订阅”订阅第一个帐号" __usage__ = "本bot可以提供b站、微博等社交媒体的消息订阅详情" "请查看本bot文档或者at本bot发送“添加订阅”订阅第一个帐号"
__module_name__ = "nonebot-bison"

View File

@ -1,10 +1,10 @@
import nonebot import nonebot
from nonebot.adapters.onebot.v11.bot import Bot from nonebot.adapters.onebot.v11.bot import Bot
from ..config import Config, NoSuchSubscribeException, NoSuchUserException
from ..platform import check_sub_target, platform_manager
from .jwt import pack_jwt from .jwt import pack_jwt
from .token_manager import token_manager from .token_manager import token_manager
from ..platform import check_sub_target, platform_manager
from ..config import Config, NoSuchUserException, NoSuchSubscribeException
async def test(): async def test():

View File

@ -1,6 +1,6 @@
import datetime
import random import random
import string import string
import datetime
from typing import Optional from typing import Optional
import jwt import jwt

View File

@ -1,16 +1,16 @@
from collections import defaultdict
import os import os
from os import path from os import path
from collections import defaultdict from typing import DefaultDict, Literal, Mapping, TypedDict
from typing import Literal, Mapping, TypedDict, DefaultDict
import nonebot import nonebot
from nonebot import logger from nonebot import logger
from tinydb import Query, TinyDB from tinydb import Query, TinyDB
from .utils import Singleton
from .types import User, Target
from .platform import platform_manager from .platform import platform_manager
from .plugin_config import plugin_config from .plugin_config import plugin_config
from .types import Target, User
from .utils import Singleton
supported_target_type = platform_manager.keys() supported_target_type = platform_manager.keys()

View File

@ -1,20 +1,20 @@
from typing import Type from typing import Type
from nonebot import on_command from nonebot import on_command
from nonebot.rule import to_me from nonebot.adapters._event import Event as AbstractEvent
from nonebot.typing import T_State
from nonebot.matcher import Matcher
from nonebot.permission import SUPERUSER
from nonebot.params import State, Depends
from nonebot.adapters.onebot.v11 import Bot, Event from nonebot.adapters.onebot.v11 import Bot, Event
from nonebot.adapters.onebot.v11.message import Message from nonebot.adapters.onebot.v11.message import Message
from nonebot.adapters._event import Event as AbstractEvent
from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER from nonebot.adapters.onebot.v11.permission import GROUP_ADMIN, GROUP_OWNER
from nonebot.matcher import Matcher
from nonebot.params import Depends, State
from nonebot.permission import SUPERUSER
from nonebot.rule import to_me
from nonebot.typing import T_State
from .config import Config from .config import Config
from .utils import parse_text
from .types import Target, Category
from .platform import check_sub_target, platform_manager from .platform import check_sub_target, platform_manager
from .types import Category, Target
from .utils import parse_text
def _gen_prompt_template(prompt: str): def _gen_prompt_template(prompt: str):

View File

@ -1,9 +1,9 @@
from pathlib import Path
from pkgutil import iter_modules
from collections import defaultdict from collections import defaultdict
from importlib import import_module from importlib import import_module
from pathlib import Path
from pkgutil import iter_modules
from .platform import Platform, NoTargetGroup from .platform import NoTargetGroup, Platform
_package_dir = str(Path(__file__).resolve().parent) _package_dir = str(Path(__file__).resolve().parent)
for (_, module_name, _) in iter_modules([_package_dir]): for (_, module_name, _) in iter_modules([_package_dir]):

View File

@ -4,8 +4,8 @@ from typing import Any, Optional
import httpx import httpx
from ..post import Post from ..post import Post
from ..types import Tag, Target, RawPost, Category from ..types import Category, RawPost, Tag, Target
from .platform import NewMessage, CategoryNotSupport from .platform import CategoryNotSupport, NewMessage
class Bilibili(NewMessage): class Bilibili(NewMessage):

View File

@ -3,8 +3,8 @@ from typing import Any, Optional
import httpx import httpx
from ..post import Post from ..post import Post
from ..types import RawPost, Target
from .platform import NewMessage from .platform import NewMessage
from ..types import Target, RawPost
class NcmArtist(NewMessage): class NcmArtist(NewMessage):

View File

@ -3,8 +3,8 @@ from typing import Any, Optional
import httpx import httpx
from ..post import Post from ..post import Post
from ..types import RawPost, Target
from .platform import NewMessage from .platform import NewMessage
from ..types import Target, RawPost
class NcmRadio(NewMessage): class NcmRadio(NewMessage):

View File

@ -1,15 +1,15 @@
import time
from dataclasses import dataclass
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections import defaultdict from collections import defaultdict
from typing import Any, Literal, Optional, Collection from dataclasses import dataclass
import time
from typing import Any, Collection, Literal, Optional
import httpx import httpx
from nonebot import logger from nonebot import logger
from ..post import Post
from ..plugin_config import plugin_config from ..plugin_config import plugin_config
from ..types import Tag, User, Target, RawPost, Category, UserSubInfo from ..post import Post
from ..types import Category, RawPost, Tag, Target, User, UserSubInfo
class CategoryNotSupport(Exception): class CategoryNotSupport(Exception):

View File

@ -1,13 +1,13 @@
import calendar import calendar
from typing import Any, Optional from typing import Any, Optional
import httpx
import feedparser
from bs4 import BeautifulSoup as bs from bs4 import BeautifulSoup as bs
import feedparser
import httpx
from ..post import Post from ..post import Post
from ..types import RawPost, Target
from .platform import NewMessage from .platform import NewMessage
from ..types import Target, RawPost
class Rss(NewMessage): class Rss(NewMessage):

View File

@ -1,14 +1,13 @@
import re
import json
import hashlib
from datetime import datetime from datetime import datetime
import hashlib
import json
import re
from typing import Any, Optional from typing import Any, Optional
import httpx
from bs4 import BeautifulSoup as bs from bs4 import BeautifulSoup as bs
import httpx
from ..types import * from ..types import *
from ..post import Post
# from .platform import Platform # from .platform import Platform

View File

@ -1,16 +1,16 @@
import base64 import base64
from io import BytesIO from dataclasses import dataclass, field
from functools import reduce from functools import reduce
from typing import Union, Optional from io import BytesIO
from dataclasses import field, dataclass from typing import Optional, Union
import httpx
from PIL import Image from PIL import Image
import httpx
from nonebot import logger from nonebot import logger
from nonebot.adapters.onebot.v11.message import Message, MessageSegment from nonebot.adapters.onebot.v11.message import Message, MessageSegment
from .utils import parse_text
from .plugin_config import plugin_config from .plugin_config import plugin_config
from .utils import parse_text
@dataclass @dataclass

View File

@ -1,16 +1,15 @@
import asyncio
import logging import logging
import nonebot
from nonebot.log import LoguruHandler
from nonebot import logger, get_driver
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
import nonebot
from nonebot import get_driver, logger
from nonebot.log import LoguruHandler
from .config import Config from .config import Config
from .types import UserSubInfo
from .platform import platform_manager from .platform import platform_manager
from .plugin_config import plugin_config from .plugin_config import plugin_config
from .send import send_msgs, do_send_msgs from .send import do_send_msgs, send_msgs
from .types import UserSubInfo
scheduler = AsyncIOScheduler(timezone="Asia/Shanghai") scheduler = AsyncIOScheduler(timezone="Asia/Shanghai")

View File

@ -1,5 +1,5 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Any, NewType, Callable, NamedTuple from typing import Any, Callable, NamedTuple, NewType
RawPost = NewType("RawPost", Any) RawPost = NewType("RawPost", Any)
Target = NewType("Target", str) Target = NewType("Target", str)

View File

@ -1,19 +1,23 @@
import os
import re
import sys
import base64
import asyncio import asyncio
import subprocess import base64
from html import escape from html import escape
import os
from pathlib import Path
import platform
import re
import subprocess
import sys
from time import asctime from time import asctime
from typing import Union, Callable, Optional, Awaitable from typing import Awaitable, Callable, Optional, Union
import nonebot
from bs4 import BeautifulSoup as bs from bs4 import BeautifulSoup as bs
from nonebot.log import logger, default_format import nonebot
from nonebot.adapters.onebot.v11.message import MessageSegment from nonebot.adapters.onebot.v11.message import MessageSegment
from nonebot.log import default_format, logger
from playwright._impl._driver import compute_driver_executable from playwright._impl._driver import compute_driver_executable
from playwright.async_api import Page, Browser, Playwright, async_playwright from playwright.async_api import Browser, Page, Playwright, async_playwright
from uvicorn import config
from uvicorn.loops import asyncio as _asyncio
from .plugin_config import plugin_config from .plugin_config import plugin_config
@ -30,6 +34,16 @@ class Singleton(type):
@nonebot.get_driver().on_startup @nonebot.get_driver().on_startup
def download_browser(): def download_browser():
if not plugin_config.bison_browser and not plugin_config.bison_use_local: if not plugin_config.bison_browser and not plugin_config.bison_use_local:
system = platform.system()
if system == "Linux":
browser_path = Path.home() / ".cache" / "ms-playwright"
elif system == "Windows":
browser_path = Path.home() / "AppData" / "Local" / "ms-playwright"
else:
raise RuntimeError("platform not supported")
if browser_path.exists() and os.listdir(str(browser_path)):
logger.warning("Browser Exists, skip")
return
env = os.environ.copy() env = os.environ.copy()
driver_executable = compute_driver_executable() driver_executable = compute_driver_executable()
env["PW_CLI_TARGET_LANG"] = "python" env["PW_CLI_TARGET_LANG"] = "python"
@ -219,4 +233,19 @@ if plugin_config.bison_filter_log:
if config.log_level is None if config.log_level is None
else config.log_level else config.log_level
) )
logger.warning("test")
# monkey patch
def asyncio_setup():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
@property
def should_reload(self):
return False
if platform.system() == "Windows":
_asyncio.asyncio_setup = asyncio_setup
config.Config.should_reload = should_reload # type:ignore
logger.warning("检测到当前为 Windows 系统,已自动注入猴子补丁")

View File

@ -1,9 +1,9 @@
import typing
from pathlib import Path from pathlib import Path
import typing
import pytest
import nonebot import nonebot
from nonebug.app import App from nonebug.app import App
import pytest
@pytest.fixture @pytest.fixture

View File

@ -1,7 +1,7 @@
import respx
import pytest
from httpx import Response from httpx import Response
from nonebug.app import App from nonebug.app import App
import pytest
import respx
from .utils import get_file, get_json from .utils import get_file, get_json

View File

@ -1,6 +1,6 @@
import pytest
from httpx import Response from httpx import Response
from nonebug.app import App from nonebug.app import App
import pytest
from .utils import get_json from .utils import get_json

View File

@ -1,9 +1,9 @@
import time import time
import respx
import pytest
from httpx import Response from httpx import Response
from nonebug.app import App from nonebug.app import App
import pytest
import respx
from .utils import get_json from .utils import get_json

View File

@ -1,9 +1,9 @@
import time import time
import respx
import pytest
from httpx import Response from httpx import Response
from nonebug.app import App from nonebug.app import App
import pytest
import respx
from .utils import get_json from .utils import get_json

View File

@ -1,8 +1,8 @@
from time import time from time import time
from typing import Any, Optional from typing import Any, Optional
import pytest
from nonebug.app import App from nonebug.app import App
import pytest
now = time() now = time()
passed = now - 3 * 60 * 60 passed = now - 3 * 60 * 60
@ -38,9 +38,9 @@ def user_info_factory(app: App, dummy_user):
@pytest.fixture @pytest.fixture
def mock_platform_without_cats_tags(app: App): def mock_platform_without_cats_tags(app: App):
from nonebot_bison.post import Post
from nonebot_bison.types import Target, RawPost
from nonebot_bison.platform.platform import NewMessage from nonebot_bison.platform.platform import NewMessage
from nonebot_bison.post import Post
from nonebot_bison.types import RawPost, Target
class MockPlatform(NewMessage): class MockPlatform(NewMessage):
@ -87,9 +87,9 @@ def mock_platform_without_cats_tags(app: App):
@pytest.fixture @pytest.fixture
def mock_platform(app: App): def mock_platform(app: App):
from nonebot_bison.post import Post
from nonebot_bison.platform.platform import NewMessage from nonebot_bison.platform.platform import NewMessage
from nonebot_bison.types import Tag, Target, RawPost, Category from nonebot_bison.post import Post
from nonebot_bison.types import Category, RawPost, Tag, Target
class MockPlatform(NewMessage): class MockPlatform(NewMessage):
@ -145,9 +145,9 @@ def mock_platform(app: App):
@pytest.fixture @pytest.fixture
def mock_platform_no_target(app: App): def mock_platform_no_target(app: App):
from nonebot_bison.platform.platform import CategoryNotSupport, NewMessage
from nonebot_bison.post import Post from nonebot_bison.post import Post
from nonebot_bison.types import Tag, Target, RawPost, Category from nonebot_bison.types import Category, RawPost, Tag, Target
from nonebot_bison.platform.platform import NewMessage, CategoryNotSupport
class MockPlatform(NewMessage): class MockPlatform(NewMessage):
@ -203,9 +203,9 @@ def mock_platform_no_target(app: App):
@pytest.fixture @pytest.fixture
def mock_platform_no_target_2(app: App): def mock_platform_no_target_2(app: App):
from nonebot_bison.post import Post
from nonebot_bison.platform.platform import NewMessage from nonebot_bison.platform.platform import NewMessage
from nonebot_bison.types import Tag, Target, RawPost, Category from nonebot_bison.post import Post
from nonebot_bison.types import Category, RawPost, Tag, Target
class MockPlatform(NewMessage): class MockPlatform(NewMessage):
@ -270,9 +270,9 @@ def mock_platform_no_target_2(app: App):
@pytest.fixture @pytest.fixture
def mock_status_change(app: App): def mock_status_change(app: App):
from nonebot_bison.post import Post
from nonebot_bison.platform.platform import StatusChange from nonebot_bison.platform.platform import StatusChange
from nonebot_bison.types import Tag, Target, RawPost, Category from nonebot_bison.post import Post
from nonebot_bison.types import Category, RawPost, Tag, Target
class MockPlatform(StatusChange): class MockPlatform(StatusChange):
@ -440,9 +440,9 @@ async def test_group(
user_info_factory, user_info_factory,
): ):
from nonebot_bison.post import Post
from nonebot_bison.platform.platform import NoTargetGroup from nonebot_bison.platform.platform import NoTargetGroup
from nonebot_bison.types import Tag, Target, RawPost, Category from nonebot_bison.post import Post
from nonebot_bison.types import Category, RawPost, Tag, Target
group_platform = NoTargetGroup([mock_platform_no_target, mock_platform_no_target_2]) group_platform = NoTargetGroup([mock_platform_no_target, mock_platform_no_target_2])
res1 = await group_platform.fetch_new_post( res1 = await group_platform.fetch_new_post(

View File

@ -1,11 +1,11 @@
from datetime import datetime from datetime import datetime
import respx
import pytest
import feedparser import feedparser
from pytz import timezone
from httpx import Response from httpx import Response
from nonebug.app import App from nonebug.app import App
import pytest
from pytz import timezone
import respx
from .utils import get_file, get_json from .utils import get_file, get_json

View File

@ -1,7 +1,7 @@
import typing import typing
import pytest
from nonebug.app import App from nonebug.app import App
import pytest
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
import sys import sys

View File

@ -1,7 +1,7 @@
import typing import typing
import pytest
from nonebug.app import App from nonebug.app import App
import pytest
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
import sys import sys

View File

@ -1,7 +1,7 @@
import typing import typing
import pytest
from nonebug.app import App from nonebug.app import App
import pytest
if typing.TYPE_CHECKING: if typing.TYPE_CHECKING:
import sys import sys