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_commit_msg: "auto update by pre-commit hooks"
repos:
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
# - repo: https://github.com/pycqa/isort
# rev: 5.10.1
# hooks:
# - id: isort
# args: ["--profile", "black", "--filter-files"]
- repo: https://github.com/psf/black
rev: 22.1.0
@ -19,4 +20,4 @@ repos:
rev: v2.5.1
hooks:
- id: prettier
types_or: [markdown]
types_or: [markdown, ts, tsx]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import App from './App';
import './index.css';
import reportWebVitals from './reportWebVitals';
import store from './store';
import {injectStore} from 'src/api/utils';
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import App from "./App";
import "./index.css";
import reportWebVitals from "./reportWebVitals";
import store from "./store";
import { injectStore } from "src/api/utils";
injectStore(store);
ReactDOM.render(
@ -14,7 +14,7 @@ ReactDOM.render(
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
document.getElementById("root")
);
// 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 React, {ReactElement, useEffect, useState} from "react";
import {useDispatch, useSelector} from 'react-redux';
import {AddModal} from 'src/component/addSubsModal';
import {SubscribeCard} from 'src/component/subscribeCard';
import {groupConfigSelector, updateGroupSubs} from 'src/store/groupConfigSlice';
import { Button, Collapse, Empty, Row } from "antd";
import React, { ReactElement, useEffect, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import { AddModal } from "src/component/addSubsModal";
import { SubscribeCard } from "src/component/subscribeCard";
import {
groupConfigSelector,
updateGroupSubs,
} from "src/store/groupConfigSlice";
interface ConfigPageProp {
tab: string
tab: string;
}
export function ConfigPage(prop: ConfigPageProp) {
const [ showModal, setShowModal ] = useState<boolean>(false);
const [ currentAddingGroupNumber, setCurrentAddingGroupNumber ] = useState('');
const [showModal, setShowModal] = useState<boolean>(false);
const [currentAddingGroupNumber, setCurrentAddingGroupNumber] = useState("");
const configData = useSelector(groupConfigSelector);
const dispatcher = useDispatch();
useEffect(() => {
dispatcher(updateGroupSubs())
dispatcher(updateGroupSubs());
}, [prop.tab, dispatcher]);
const clickNew = (groupNumber: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
setShowModal(_ => true);
setCurrentAddingGroupNumber(groupNumber);
e.stopPropagation();
}
const clickNew =
(groupNumber: string) => (e: React.MouseEvent<HTMLButtonElement>) => {
setShowModal((_) => true);
setCurrentAddingGroupNumber(groupNumber);
e.stopPropagation();
};
if (Object.keys(configData).length === 0) {
return <Empty />
return <Empty />;
} else {
let groups: Array<ReactElement> = [];
for (let key of Object.keys(configData)) {
let value = configData[key];
groups.push(
<Collapse.Panel key={key} header={
<span>{`${key} - ${value.name}`}<Button style={{float: "right"}} onClick={clickNew(key)}></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} />)}
<Collapse.Panel
key={key}
header={
<span>
{`${key} - ${value.name}`}
<Button style={{ float: "right" }} onClick={clickNew(key)}>
</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>
</Collapse.Panel>
)
);
}
return (
<div>
<Collapse>
{groups}
</Collapse>
<AddModal groupNumber={currentAddingGroupNumber} showModal={showModal}
refresh={() => dispatcher(updateGroupSubs())}
setShowModal={(s: boolean) => setShowModal(_ => s)} />
</div>
)
<div>
<Collapse>{groups}</Collapse>
<AddModal
groupNumber={currentAddingGroupNumber}
showModal={showModal}
refresh={() => dispatcher(updateGroupSubs())}
setShowModal={(s: boolean) => setShowModal((_) => s)}
/>
</div>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,5 @@
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux";
import {AppDispatch, RootState} from ".";
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux";
import { AppDispatch, RootState } from ".";
export const useAppDispacher = () => useDispatch<AppDispatch>()
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
export const useAppDispacher = () => useDispatch<AppDispatch>();
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 globalConfSlice from "./globalConfSlice";
import groupConfigSlice from './groupConfigSlice';
import groupConfigSlice from "./groupConfigSlice";
const store = configureStore({
reducer: {
login: loginSlice,
globalConf: globalConfSlice,
groupConfig: groupConfigSlice,
}
})
},
});
export default store;

View File

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

View File

@ -1,34 +1,34 @@
interface QQGroup {
id: string,
name: string,
id: string;
name: string;
}
export interface LoginStatus {
login: boolean
type: string
name: string
id: string
login: boolean;
type: string;
name: string;
id: string;
// groups: Array<QQGroup>
token: string,
failed: boolean,
token: string;
failed: boolean;
}
export type LoginContextType = {
login: LoginStatus
save: (status: LoginStatus) => void
}
login: LoginStatus;
save: (status: LoginStatus) => void;
};
export interface SubscribeConfig {
platformName: string
target: string
targetName: string
cats: Array<number>
tags: Array<string>
platformName: string;
target: string;
targetName: string;
cats: Array<number>;
tags: Array<string>;
}
export interface GlobalConf {
platformConf: AllPlatformConf,
loaded: boolean
platformConf: AllPlatformConf;
loaded: boolean;
}
export interface AllPlatformConf {
@ -36,34 +36,34 @@ export interface AllPlatformConf {
}
export interface CategoryConfig {
[idx: number]: string
[idx: number]: string;
}
export interface PlatformConfig {
name: string
categories: CategoryConfig
enabledTag: boolean,
platformName: string,
hasTarget: boolean
name: string;
categories: CategoryConfig;
enabledTag: boolean;
platformName: string;
hasTarget: boolean;
}
export interface TokenResp {
status: number,
token: string,
type: string,
id: string
name: string
status: number;
token: string;
type: string;
id: string;
name: string;
}
export interface SubscribeGroupDetail {
name: string,
subscribes: Array<SubscribeConfig>
name: string;
subscribes: Array<SubscribeConfig>;
}
export interface SubscribeResp {
[idx: string]: SubscribeGroupDetail
[idx: string]: SubscribeGroupDetail;
}
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 \
libxdamage1 libxext6 libxfixes3 libxrandr2 libxshmfence1 \
&& rm -rf /var/lib/apt/lists/*
RUN pip install playwright && playwright install chromium
COPY ./pyproject.toml ./poetry.lock* /app/
RUN poetry install --no-root --no-dev
RUN playwright install chromium
ADD src /app/src
ADD bot.py /app/
ENV HOST=0.0.0.0

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 \
libxdamage1 libxext6 libxfixes3 libxrandr2 libxshmfence1 \
&& rm -rf /var/lib/apt/lists/*
RUN pip install playwright && playwright install chromium
COPY ./pyproject.toml ./poetry.lock* ./bot.py /app/
RUN poetry add nonebot-plugin-sentry && \
sed '/nonebot.load_builtin_plugins()/a nonebot.load_plugin("nonebot_plugin_sentry")' -i bot.py
RUN poetry install --no-root --no-dev
RUN playwright install chromium
ADD src /app/src
ENV HOST=0.0.0.0
CMD ["python", "bot.py"]

View File

@ -1,6 +1,6 @@
[tool.poetry]
name = "nonebot-bison"
version = "0.4.3"
version = "0.4.4"
description = "Subscribe message from social medias"
authors = ["felinae98 <felinae225@qq.com>"]
license = "MIT"
@ -61,16 +61,3 @@ markers = [
"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 nonebot import on_request
from nonebot.log import logger
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)

View File

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

View File

@ -1,10 +1,10 @@
import nonebot
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 .token_manager import token_manager
from ..platform import check_sub_target, platform_manager
from ..config import Config, NoSuchUserException, NoSuchSubscribeException
async def test():

View File

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

View File

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

View File

@ -1,20 +1,20 @@
from typing import Type
from nonebot import on_command
from nonebot.rule import to_me
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._event import Event as AbstractEvent
from nonebot.adapters.onebot.v11 import Bot, Event
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.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 .utils import parse_text
from .types import Target, Category
from .platform import check_sub_target, platform_manager
from .types import Category, Target
from .utils import parse_text
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 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)
for (_, module_name, _) in iter_modules([_package_dir]):

View File

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

View File

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

View File

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

View File

@ -1,15 +1,15 @@
import time
from dataclasses import dataclass
from abc import ABC, abstractmethod
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
from nonebot import logger
from ..post import Post
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):

View File

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

View File

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

View File

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

View File

@ -1,16 +1,15 @@
import asyncio
import logging
import nonebot
from nonebot.log import LoguruHandler
from nonebot import logger, get_driver
from apscheduler.schedulers.asyncio import AsyncIOScheduler
import nonebot
from nonebot import get_driver, logger
from nonebot.log import LoguruHandler
from .config import Config
from .types import UserSubInfo
from .platform import platform_manager
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")

View File

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

View File

@ -1,19 +1,23 @@
import os
import re
import sys
import base64
import asyncio
import subprocess
import base64
from html import escape
import os
from pathlib import Path
import platform
import re
import subprocess
import sys
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 nonebot.log import logger, default_format
import nonebot
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.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
@ -30,6 +34,16 @@ class Singleton(type):
@nonebot.get_driver().on_startup
def download_browser():
if not plugin_config.bison_browser and not plugin_config.bison_use_local:
system = platform.system()
if system == "Linux":
browser_path = Path.home() / ".cache" / "ms-playwright"
elif system == "Windows":
browser_path = Path.home() / "AppData" / "Local" / "ms-playwright"
else:
raise RuntimeError("platform not supported")
if browser_path.exists() and os.listdir(str(browser_path)):
logger.warning("Browser Exists, skip")
return
env = os.environ.copy()
driver_executable = compute_driver_executable()
env["PW_CLI_TARGET_LANG"] = "python"
@ -219,4 +233,19 @@ if plugin_config.bison_filter_log:
if config.log_level is None
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
import typing
import pytest
import nonebot
from nonebug.app import App
import pytest
@pytest.fixture

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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