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

@ -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]

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

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

@ -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();

@ -1,37 +1,33 @@
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">
@ -42,8 +38,9 @@ function App() {
</Route>
</Switch>
</Router>
}
</>;
)}
</>
);
}
export default App;

@ -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);
}

@ -1,32 +1,40 @@
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');
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}`;
config.headers["Authorization"] = `Bearer ${token}`;
} else {
throw new axios.Cancel('User not login');
throw new axios.Cancel("User not login");
}
}
return config;
}, function (error) {
},
function (error) {
return Promise.reject(error);
});
}
);
axios.interceptors.response.use(function (response) {
axios.interceptors.response.use(
function (response) {
// const data = response.data;
// const parseToMap = (item: any): any => {
// if (item instanceof Array) {
@ -43,9 +51,11 @@ axios.interceptors.response.use(function (response) {
// }
// response.data = parseToMap(data);
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());
}
return Promise.reject(error);
});
}
);

@ -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)
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: ''
})
})
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(() => {
if (initVal) {
// patch
updateSubscribe(groupNumber, newVal).then(() => {
setConfirmLoading(false);
setShowModal(false);
form.resetFields();
refresh();
});
} else {
addSubscribe(groupNumber, newVal)
.then(() => {
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} >
}, [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 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) => {
<Form.Item
label="账号"
name="target"
rules={[
{ required: hasTarget, message: "请输入账号" },
{
validator: async (_, value) => {
try {
const res = await getTargetName(form.getFieldValue('platformName'), value);
const res = await getTargetName(
form.getFieldValue("platformName"),
value
);
if (res.targetName) {
form.setFieldsValue({
targetName: res.targetName
})
return Promise.resolve()
targetName: res.targetName,
});
return Promise.resolve();
} else {
form.setFieldsValue({
targetName: ''
})
return Promise.reject("账号不正确,请重新检查账号")
targetName: "",
});
return Promise.reject("账号不正确,请重新检查账号");
}
} 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 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"
<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 ?
"请选择要订阅的分类" : "本平台不支持分类"}>
placeholder={
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)}>
{categories[parseInt(indexStr)]}
</Select.Option>
)
}
))}
</Select>
</Form.Item>
<Form.Item label="订阅Tag" name="tags">
<InputTagCustom disabled={!enabledTag}/>
<InputTagCustom disabled={!enabledTag} />
</Form.Item>
</Form>
</Modal>
);
}

@ -1,31 +1,31 @@
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(() => {
@ -35,66 +35,80 @@ export function InputTag(prop: InputTagProp) {
}, [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);
setValue((_) => newTags);
if (prop.onChange) {
prop.onChange(newTags);
}
setEditInputIndex(_ => -1);
setEditInputValue(_ => '');
}
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);
<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>
@ -103,20 +117,35 @@ export function InputTag(prop: InputTagProp) {
<Tooltip title={tag} key={tag}>
{tagElem}
</Tooltip>
) : ( tagElem );
) : (
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>
)}
</>
);
}

@ -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) =>
};
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.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) => () => {
const reload = () => dispatcher(updateGroupSubs());
const handleDelete =
(groupNumber: string, platformName: string, target: string) => () => {
delSubscribe(groupNumber, platformName, target).then(() => {
reload()
})
}
reload();
});
};
return (
<Col span={8} key={`${config.platformName}-${config.target}`}>
<Card title={`${platformConf.name} - ${config.targetName}`}
<Card
title={`${platformConf.name} - ${config.targetName}`}
actions={[
<Tooltip title="编辑">
<EditOutlined onClick={()=>{setShowEditModal(state => !state)}}/>
<EditOutlined
onClick={() => {
setShowEditModal((state) => !state);
}}
/>
</Tooltip>,
<Tooltip title="添加到其他群">
<CopyOutlined onClick={()=>{setShowModal(state => !state)}}/>
<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
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> }
{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>}
{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>}
{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}/>
<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>
)
);
}

@ -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

@ -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);
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}
<Collapse>{groups}</Collapse>
<AddModal
groupNumber={currentAddingGroupNumber}
showModal={showModal}
refresh={() => dispatcher(updateGroupSubs())}
setShowModal={(s: boolean) => setShowModal(_ => s)} />
setShowModal={(s: boolean) => setShowModal((_) => s)}
/>
</div>
)
);
}
}

@ -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 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>
}
<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 style={{ margin: "24px", background: "#fff", minHeight: "640px" }}>
{tab === "manage" ? <ConfigPage tab={tab} /> : null}
</div>
</Layout.Content>
</Layout>
)
);
}

@ -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
code: string;
}
export function Auth() {
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> :
}, [code, dispatch]);
return (
<>
{loginState.login ? (
<Redirect to={{ pathname: "/admin" }} />
) : loginState.failed ? (
<div></div>
) : (
<div>Logining...</div>
}
</>;
)}
</>
);
}

@ -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);

@ -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";

@ -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;

@ -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;

@ -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;

@ -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;

@ -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,
localStorage.setItem(
"loginInfo",
JSON.stringify({
type: res.type,
name: res.name,
id: res.id,
}))
localStorage.setItem('token', res.token)
})
);
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;

@ -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;
}

@ -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

@ -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"]

@ -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

@ -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)

@ -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"

@ -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():

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

@ -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()

@ -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):

@ -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]):

@ -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):

@ -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):

@ -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):

@ -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):

@ -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):

@ -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

@ -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

@ -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")

@ -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)

@ -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 系统,已自动注入猴子补丁")

@ -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

@ -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

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

@ -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

@ -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

@ -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(

@ -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

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

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

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