Merge branch 'admin-page' into dev

This commit is contained in:
felinae98 2021-11-26 14:59:45 +08:00
commit 514a1c4314
No known key found for this signature in database
GPG Key ID: 00C8B010587FF610
44 changed files with 13777 additions and 17 deletions

View File

@ -6,6 +6,7 @@ orbs:
# so you dont have to copy and paste it everywhere.
# See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/python
python: circleci/python@1.4
node: circleci/node@4.7.0
# poetry: frameio/poetry@0.21.0
swissknife: roopakv/swissknife@0.59.0
docker: circleci/docker@1.7.0
@ -13,7 +14,13 @@ orbs:
workflows:
build-test-publish:
jobs:
- build-frontend:
filters:
tags:
only: /.*/
- test:
requires:
- build-frontend
filters:
tags:
only: /.*/
@ -25,7 +32,7 @@ workflows:
ignore: /.*/
tags:
only: /^v.*/
- docker/publish:
- docker/publish: &docker-push
requires:
- test
filters:
@ -40,8 +47,30 @@ workflows:
update-description: true
docker-username: DOCKERHUB_USERNAME
docker-password: DOCKERHUB_PASSWORD
- docker/publish:
<<: *docker-push
filters:
tags:
ignore: /.*/
tag: ${CIRCLE_BRANCH}
jobs:
build-frontend:
docker:
- image: cimg/node:16.13.0
steps:
- checkout
- node/install-packages:
app-dir: ./admin-frontend
pkg-manager: yarn
- run:
name: yarn build
working_directory: ./admin-frontend
command: yarn build
- persist_to_workspace:
root: .
paths:
- "src/plugins/nonebot_bison/admin_page/dist/"
test:
docker:
- image: cimg/python:3.9

1
.gitignore vendored
View File

@ -318,3 +318,4 @@ dist
data*/*
.env.*
.vim/*
!dist/.gitkeep

View File

@ -1,3 +1,8 @@
FROM node:16 as frontend
ADD . /app
WORKDIR /app/admin-frontend
RUN yarn && yarn build
FROM python:3.9
RUN python3 -m pip install poetry && poetry config virtualenvs.create false
WORKDIR /app
@ -6,5 +11,6 @@ RUN poetry install --no-root --no-dev
# RUN PYPPETEER_DOWNLOAD_HOST='http://npm.taobao.org/mirrors' pyppeteer-install
ADD src /app/src
ADD bot.py /app/
COPY --from=frontend /app/src/plugins/nonebot_bison/admin_page/dist /app/src/plugins/nonebot_bison/admin_page/dist
ENV HOST=0.0.0.0
CMD ["python", "bot.py"]

23
admin-frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

46
admin-frontend/README.md Normal file
View File

@ -0,0 +1,46 @@
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

View File

@ -0,0 +1,55 @@
{
"name": "admin-frontend",
"version": "0.1.0",
"private": true,
"homepage": "bison",
"proxy": "http://localhost:8080",
"dependencies": {
"@ant-design/icons": "^4.6.4",
"@testing-library/jest-dom": "^5.11.4",
"@testing-library/react": "^11.1.0",
"@testing-library/user-event": "^12.1.10",
"@types/jest": "^26.0.15",
"@types/node": "^12.0.0",
"@types/react": "^17.0.0",
"@types/react-dom": "^17.0.0",
"antd": "^4.16.13",
"axios": "^0.21.4",
"lodash": "^4.17.21",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^5.3.0",
"react-scripts": "4.0.3",
"typescript": "^4.1.2",
"web-vitals": "^1.0.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build && cp -r -f build/* ../src/plugins/nonebot_bison/admin_page/dist",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/lodash": "^4.14.175",
"@types/react-router-dom": "^5.3.0",
"react-app-rewired": "^2.1.8"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

View File

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

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

View File

@ -0,0 +1,60 @@
import React, { useContext, useEffect, useState } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import './App.css';
import { LoginContext, loginContextDefault, GlobalConfContext } from './utils/context';
import { LoginStatus, GlobalConf, AllPlatformConf } from './utils/type';
import { Admin } from './pages/admin';
import { getGlobalConf } from './api/config';
import { Auth } from './pages/auth';
import 'antd/dist/antd.css';
function LoginSwitch() {
const {login, save} = useContext(LoginContext);
if (login.login) {
return <Admin />;
} else {
return (
<div>
not login
<button onClick={() => save({
login: true, type: 'admin', name: '', id: '123', token: ''
})}>1</button>
</div>
)
}
}
function App() {
const [loginStatus, setLogin] = useState(loginContextDefault.login);
const [globalConf, setGlobalConf] = useState<GlobalConf>({platformConf: {} as AllPlatformConf, loaded: false});
// const globalConfContext = useContext(GlobalConfContext);
const save = (login: LoginStatus) => setLogin(_ => login);
useEffect(() => {
const fetchGlobalConf = async () => {
const res = await getGlobalConf();
setGlobalConf(_ => {return {...res, loaded: true}});
};
fetchGlobalConf();
}, []);
return (
<LoginContext.Provider value={{login: loginStatus, save}}>
<GlobalConfContext.Provider value={globalConf}>
{ globalConf.loaded &&
<Router basename="/bison">
<Switch>
<Route path="/auth/:code">
<Auth />
</Route>
<Route path="/admin/">
<LoginSwitch />
</Route>
</Switch>
</Router>
}
</GlobalConfContext.Provider>
</LoginContext.Provider>
);
}
export default App;

View File

@ -0,0 +1,33 @@
import axios from "axios";
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`);
return res.data;
}
export async function auth(token: string): Promise<TokenResp> {
const res = await axios.get<TokenResp>(`${baseUrl}auth`, {params: {token}});
return res.data;
}
export async function getSubscribe(): Promise<SubscribeResp> {
const res = await axios.get(`${baseUrl}subs`);
return res.data;
}
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}})
return res.data;
}
export async function delSubscribe(groupNumber: string, platformName: string, target: string) {
const res = await axios.delete(`${baseUrl}subs`, {params: {groupNumber, platformName, target}});
return res.data;
}

View File

@ -0,0 +1,40 @@
import axios from "axios";
// import { useContext } from 'react';
// import { LoginContext } from "../utils/context";
export const baseUrl = '/bison/api/'
// 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 = sessionStorage.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);
});
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;
});

View File

@ -0,0 +1,117 @@
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
color?: LiteralUnion<PresetColorType | PresetStatusColorType, 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 inputRef = useRef(null as any);
const editInputRef = useRef(null as any);
useEffect(() => {
if (inputVisible) {
inputRef.current.focus()
}
}, [inputVisible]);
useEffect(() => {
if (editInputIndex !== -1) {
editInputRef.current.focus();
}
}, [editInputIndex]);
const handleClose = (removedTag: string) => {
const tags = value.filter(tag => tag !== removedTag);
setValue(_ => tags);
if (prop.onChange) {
prop.onChange(tags);
}
}
const showInput = () => {
setInputVisible(_ => true);
}
const handleInputConfirm = () => {
if (inputValue && value.indexOf(inputValue) === -1) {
const newVal = [...value, inputValue];
setValue(_ => newVal);
if (prop.onChange) {
prop.onChange(newVal);
}
}
setInputVisible(_ => false);
setInputValue(_ => '');
}
const handleEditInputChange = (e: any) => {
setEditInputValue(_ => e.target.value);
}
const handleEditInputConfirm = () => {
const newTags = value.slice();
newTags[editInputIndex] = editInputValue;
setValue(_ => newTags);
if (prop.onChange) {
prop.onChange(newTags);
}
setEditInputIndex(_ => -1);
setEditInputValue(_ => '');
}
const handleInputChange = (e: any) => {
setInputValue(e.target.value);
}
return (
<>
{ value.map((tag, index) => {
if (editInputIndex === index) {
return (
<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>
);
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} />
)}
{!inputVisible && (
<Tag className="site-tag-plus" onClick={showInput} style={{background: '#fff', border: 'dashed thin', borderColor: '#bfbfbf' }}>
<PlusOutlined/> {prop.addText || "Add Tag"}
</Tag>
)}
</>
);
}

View File

@ -0,0 +1,94 @@
import {CopyOutlined, DeleteOutlined} from '@ant-design/icons';
import {Card, Col, Form, message, Popconfirm, Select, Tag, Tooltip} from 'antd';
import Modal from 'antd/lib/modal/Modal';
import React, {useContext, useState} from "react";
import {addSubscribe, delSubscribe} from 'src/api/config';
import {GlobalConfContext} from "src/utils/context";
import {PlatformConfig, SubscribeConfig, SubscribeResp} from 'src/utils/type';
interface CopyModalProp {
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) {
await addSubscribe(selectedGroup, config);
}
}
const handleOk = () => {
if (selectedGroups.length === 0) {
message.error("请至少选择一个目标群");
} else{
setConfirmLoading(true)
postReqs(selectedGroups, config).then(() => {
setConfirmLoading(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>
</Modal>
}
interface SubscribeCardProp {
groupNumber: string
config: SubscribeConfig
groupSubscribes: SubscribeResp
reload: () => void
}
export function SubscribeCard({groupNumber, config, reload, groupSubscribes}: SubscribeCardProp) {
const globalConf = useContext(GlobalConfContext);
const [showModal, setShowModal] = useState(false)
const platformConf = globalConf.platformConf[config.platformName] as PlatformConfig;
const handleDelete = (groupNumber: string, platformName: string, target: string) => () => {
delSubscribe(groupNumber, platformName, target).then(() => {
reload()
})
}
return (
<Col span={6} key={`${config.platformName}-${config.target}`}>
<Card title={`${platformConf.name} - ${config.targetName}`}
actions={[
<Popconfirm title={`确定要删除 ${platformConf.name} - ${config.targetName}`}
onConfirm={handleDelete(groupNumber, config.platformName, config.target || 'default')}>
<Tooltip title="删除" ><DeleteOutlined /></Tooltip>
</Popconfirm>,
<Tooltip title="添加到其他群">
<CopyOutlined onClick={()=>{setShowModal(state => !state)}}/>
</Tooltip>
]}>
<Form labelCol={{ span: 6 }}>
<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="red"></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="red">Tag</Tag>}
</Form.Item>
</Form>
</Card>
<CopyModal setShowModal={setShowModal} reload={reload} currentGroupNumber={groupNumber}
showModal={showModal} config={config} groups={groupSubscribes}/>
</Col>
)
}

View File

@ -0,0 +1,13 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,5 @@
.layout-side .user {
height: 32px;
margin: 16px;
background: rgba(255, 255, 255, 0.3);
}

View File

@ -0,0 +1,150 @@
import { Form, Input, Modal, Select, Tag } from 'antd';
import React, { useContext, useState } from "react";
import { addSubscribe, getTargetName } from 'src/api/config';
import { InputTag } from 'src/component/inputTag';
import { GlobalConfContext } from "src/utils/context";
import { CategoryConfig } from 'src/utils/type';
interface InputTagCustomProp {
value?: Array<string>,
onChange?: (value: Array<string>) => void,
disabled?: boolean
}
function InputTagCustom(prop: InputTagCustomProp) {
const [value, setValue] = useState(prop.value || []);
const handleSetValue = (newVal: Array<string>) => {
setValue(() => newVal);
if (prop.onChange) {
prop.onChange(newVal);
}
}
return (
<>
{
prop.disabled ? <Tag color="red"></Tag>:
<>
{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
}
export function AddModal(prop: AddModalProp) {
const [ confirmLoading, setConfirmLoading ] = useState<boolean>(false);
const { platformConf } = useContext(GlobalConfContext);
const [ hasTarget, setHasTarget ] = useState(false);
const [ categories, setCategories ] = useState({} as CategoryConfig);
const [ enabledTag, setEnableTag ] = useState(false);
const [ form ] = Form.useForm();
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: ''
})
})
} else {
form.setFieldsValue({
targetName: '',
target: ''
})
}
}
const handleSubmit = (value: any) => {
let newVal = Object.assign({}, value)
if (typeof newVal.tags != 'object') {
newVal.tags = []
}
if (newVal.target === '') {
newVal.target = 'default'
}
addSubscribe(prop.groupNumber, newVal)
.then(() => {
setConfirmLoading(false);
prop.setShowModal(false);
prop.refresh();
});
}
const handleModleFinish = () => {
form.submit();
setConfirmLoading(() => true);
}
return <Modal title="添加订阅" visible={prop.showModal}
confirmLoading={confirmLoading} onCancel={() => prop.setShowModal(false)}
onOk={handleModleFinish}>
<Form form={form} labelCol={{ span: 6 }} name="b" onFinish={handleSubmit}
initialValues={{tags: [], cats: []}}>
<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)]}
</Select.Option>
)
}
</Select>
</Form.Item>
<Form.Item label="订阅Tag" name="tags">
<InputTagCustom disabled={!enabledTag}/>
</Form.Item>
</Form>
</Modal>
}

View File

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

View File

@ -0,0 +1,36 @@
import {BugOutlined, SettingOutlined} from '@ant-design/icons';
import {Layout, Menu} from 'antd';
import React, {useContext, useState} from "react";
import {LoginContext} from "src/utils/context";
import './admin.css';
import {ConfigPage} from './configPage';
export function Admin() {
const { login } = useContext(LoginContext);
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>
)
}

View File

@ -0,0 +1,27 @@
import React, {useContext, useEffect, useState} from "react";
import { useParams } from "react-router";
import { auth } from '../api/config';
import { LoginContext } from '../utils/context';
import { Redirect } from 'react-router-dom'
interface AuthParam {
code: string
}
export function Auth() {
const { code } = useParams<AuthParam>();
const [ content, contentUpdate ] = useState(<div>Logining...</div>);
const { save } = useContext(LoginContext);
useEffect(() => {
const loginFun = async () => {
const resp = await auth(code);
if (resp.status === 200) {
save({login: true, type: resp.type, name: resp.name, id: resp.id, token: resp.token});
contentUpdate(_ => <Redirect to={{pathname: '/admin'}} />);
sessionStorage.setItem('token', resp.token);
} else {
contentUpdate(_ => <div></div>);
}
}
loginFun();
}, [code, save])
return content;
}

1
admin-frontend/src/react-app-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

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

View File

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// 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';

View File

@ -0,0 +1,17 @@
import { createContext } from "react";
import { LoginContextType, GlobalConf } from "./type";
export const loginContextDefault: LoginContextType = {
login: {
login: false,
type: '',
name: '',
id: '123',
// groups: [],
token: ''
},
save: () => {}
};
export const LoginContext = createContext(loginContextDefault);
export const GlobalConfContext = createContext<GlobalConf>({platformConf: {}, loaded: false});

View File

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

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": "./"
},
"include": [
"src"
]
}

12218
admin-frontend/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

216
poetry.lock generated
View File

@ -1,3 +1,11 @@
[[package]]
name = "aiofiles"
version = "0.7.0"
description = "File support for asyncio."
category = "main"
optional = false
python-versions = ">=3.6,<4.0"
[[package]]
name = "anyio"
version = "3.3.4"
@ -113,6 +121,14 @@ soupsieve = ">1.2"
html5lib = ["html5lib"]
lxml = ["lxml"]
[[package]]
name = "bidict"
version = "0.21.4"
description = "The bidirectional mapping library for Python."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "bs4"
version = "0.0.1"
@ -181,6 +197,17 @@ category = "dev"
optional = false
python-versions = ">=3.5"
[[package]]
name = "expiringdict"
version = "1.2.1"
description = "Dictionary with auto-expiring values for caching purposes"
category = "main"
optional = false
python-versions = "*"
[package.extras]
tests = ["dill", "coverage", "coveralls", "mock", "nose"]
[[package]]
name = "fastapi"
version = "0.68.2"
@ -240,7 +267,7 @@ python-versions = ">=3.6.1"
[[package]]
name = "httpcore"
version = "0.14.2"
version = "0.14.3"
description = "A minimal low-level HTTP client."
category = "main"
optional = false
@ -387,6 +414,20 @@ parso = ">=0.8.0,<0.9.0"
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"]
[[package]]
name = "jinja2"
version = "3.0.3"
description = "A very fast and expressive template engine."
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
MarkupSafe = ">=2.0"
[package.extras]
i18n = ["Babel (>=2.7)"]
[[package]]
name = "loguru"
version = "0.5.3"
@ -402,6 +443,14 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
[package.extras]
dev = ["codecov (>=2.0.15)", "colorama (>=0.3.4)", "flake8 (>=3.7.7)", "tox (>=3.9.0)", "tox-travis (>=0.12)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "Sphinx (>=2.2.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "black (>=19.10b0)", "isort (>=5.1.1)"]
[[package]]
name = "markupsafe"
version = "2.0.1"
description = "Safely add untrusted strings to HTML/XML markup."
category = "main"
optional = false
python-versions = ">=3.6"
[[package]]
name = "matplotlib-inline"
version = "0.1.3"
@ -450,14 +499,14 @@ aiohttp = ["aiohttp[speedups] (>=3.7.4,<4.0.0)"]
[[package]]
name = "packaging"
version = "21.2"
version = "21.3"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.6"
[package.dependencies]
pyparsing = ">=2.0.2,<3"
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]]
name = "parso"
@ -577,13 +626,30 @@ category = "main"
optional = false
python-versions = "*"
[[package]]
name = "pyjwt"
version = "2.3.0"
description = "JSON Web Token implementation in Python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
crypto = ["cryptography (>=3.3.1)"]
dev = ["sphinx", "sphinx-rtd-theme", "zope.interface", "cryptography (>=3.3.1)", "pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)", "mypy", "pre-commit"]
docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"]
tests = ["pytest (>=6.0.0,<7.0.0)", "coverage[toml] (==5.0.4)"]
[[package]]
name = "pyparsing"
version = "2.4.7"
version = "3.0.6"
description = "Python parsing module"
category = "dev"
optional = false
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
python-versions = ">=3.6"
[package.extras]
diagrams = ["jinja2", "railroad-diagrams"]
[[package]]
name = "pyppeteer"
@ -647,6 +713,34 @@ python-versions = ">=3.5"
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-engineio"
version = "4.3.0"
description = "Engine.IO server and client for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.extras]
asyncio_client = ["aiohttp (>=3.4)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
[[package]]
name = "python-socketio"
version = "5.5.0"
description = "Socket.IO server and client for Python"
category = "main"
optional = false
python-versions = ">=3.6"
[package.dependencies]
bidict = ">=0.21.0"
python-engineio = ">=4.3.0"
[package.extras]
asyncio_client = ["aiohttp (>=3.4)"]
client = ["requests (>=2.21.0)", "websocket-client (>=0.54.0)"]
[[package]]
name = "pytz"
version = "2021.3"
@ -924,9 +1018,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
[metadata]
lock-version = "1.1"
python-versions = "^3.9"
content-hash = "5e71b794d68042efbfb810bfd56d652cf5439fe02f6bb02ed31142ecb0e5adc7"
content-hash = "77b95ff5c357cba44f6102ef29a07e59fa5b6a2abc814e6f97c41c9531f597ab"
[metadata.files]
aiofiles = [
{file = "aiofiles-0.7.0-py3-none-any.whl", hash = "sha256:c67a6823b5f23fcab0a2595a289cec7d8c863ffcb4322fb8cd6b90400aedfdbc"},
{file = "aiofiles-0.7.0.tar.gz", hash = "sha256:a1c4fc9b2ff81568c83e21392a82f344ea9d23da906e4f6a52662764545e19d4"},
]
anyio = [
{file = "anyio-3.3.4-py3-none-any.whl", hash = "sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66"},
{file = "anyio-3.3.4.tar.gz", hash = "sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff"},
@ -963,6 +1061,10 @@ beautifulsoup4 = [
{file = "beautifulsoup4-4.10.0-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"},
{file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"},
]
bidict = [
{file = "bidict-0.21.4-py3-none-any.whl", hash = "sha256:3ac67daa353ecf853a1df9d3e924f005e729227a60a8dbada31a4c31aba7f654"},
{file = "bidict-0.21.4.tar.gz", hash = "sha256:42c84ffbe6f8de898af6073b4be9ea7ccedcd78d3474aa844c54e49d5a079f6f"},
]
bs4 = [
{file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"},
]
@ -1040,6 +1142,9 @@ decorator = [
{file = "decorator-5.1.0-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
]
expiringdict = [
{file = "expiringdict-1.2.1.tar.gz", hash = "sha256:fe2ba427220425c3c8a3d29f6d2e2985bcee323f8bcd4021e68ebefbd90d8250"},
]
fastapi = [
{file = "fastapi-0.68.2-py3-none-any.whl", hash = "sha256:36bcdd3dbea87c586061005e4a40b9bd0145afd766655b4e0ec1d8870b32555c"},
{file = "fastapi-0.68.2.tar.gz", hash = "sha256:38526fc46bda73f7ec92033952677323c16061e70a91d15c95f18b11895da494"},
@ -1061,8 +1166,8 @@ hpack = [
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
]
httpcore = [
{file = "httpcore-0.14.2-py3-none-any.whl", hash = "sha256:47d7c8f755719d4a57be0b6e022897e9e963bf9ce4b15b9cc006a38a1cfa2932"},
{file = "httpcore-0.14.2.tar.gz", hash = "sha256:ff8f8b9434ec4823f95a30596fbe78039913e706d3e598b0b8955b1e1828e093"},
{file = "httpcore-0.14.3-py3-none-any.whl", hash = "sha256:9a98d2416b78976fc5396ff1f6b26ae9885efbb3105d24eed490f20ab4c95ec1"},
{file = "httpcore-0.14.3.tar.gz", hash = "sha256:d10162a63265a0228d5807964bd964478cbdb5178f9a2eedfebb2faba27eef5d"},
]
httptools = [
{file = "httptools-0.2.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5"},
@ -1112,10 +1217,85 @@ jedi = [
{file = "jedi-0.18.1-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"},
{file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"},
]
jinja2 = [
{file = "Jinja2-3.0.3-py3-none-any.whl", hash = "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8"},
{file = "Jinja2-3.0.3.tar.gz", hash = "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"},
]
loguru = [
{file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"},
{file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"},
]
markupsafe = [
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d8446c54dc28c01e5a2dbac5a25f071f6653e6e40f3a8818e8b45d790fe6ef53"},
{file = "MarkupSafe-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36bc903cbb393720fad60fc28c10de6acf10dc6cc883f3e24ee4012371399a38"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d7d807855b419fc2ed3e631034685db6079889a1f01d5d9dac950f764da3dad"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:add36cb2dbb8b736611303cd3bfcee00afd96471b09cda130da3581cbdc56a6d"},
{file = "MarkupSafe-2.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:168cd0a3642de83558a5153c8bd34f175a9a6e7f6dc6384b9655d2697312a646"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4dc8f9fb58f7364b63fd9f85013b780ef83c11857ae79f2feda41e270468dd9b"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:20dca64a3ef2d6e4d5d615a3fd418ad3bde77a47ec8a23d984a12b5b4c74491a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:cdfba22ea2f0029c9261a4bd07e830a8da012291fbe44dc794e488b6c9bb353a"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win32.whl", hash = "sha256:99df47edb6bda1249d3e80fdabb1dab8c08ef3975f69aed437cb69d0a5de1e28"},
{file = "MarkupSafe-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:e0f138900af21926a02425cf736db95be9f4af72ba1bb21453432a07f6082134"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:f9081981fe268bd86831e5c75f7de206ef275defcb82bc70740ae6dc507aee51"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:0955295dd5eec6cb6cc2fe1698f4c6d84af2e92de33fbcac4111913cd100a6ff"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0446679737af14f45767963a1a9ef7620189912317d095f2d9ffa183a4d25d2b"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:f826e31d18b516f653fe296d967d700fddad5901ae07c622bb3705955e1faa94"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:fa130dd50c57d53368c9d59395cb5526eda596d3ffe36666cd81a44d56e48872"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:905fec760bd2fa1388bb5b489ee8ee5f7291d692638ea5f67982d968366bef9f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf5d821ffabf0ef3533c39c518f3357b171a1651c1ff6827325e4489b0e46c3c"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0d4b31cc67ab36e3392bbf3862cfbadac3db12bdd8b02a2731f509ed5b829724"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:baa1a4e8f868845af802979fcdbf0bb11f94f1cb7ced4c4b8a351bb60d108145"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:deb993cacb280823246a026e3b2d81c493c53de6acfd5e6bfe31ab3402bb37dd"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:63f3268ba69ace99cab4e3e3b5840b03340efed0948ab8f78d2fd87ee5442a4f"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:8d206346619592c6200148b01a2142798c989edcb9c896f9ac9722a99d4e77e6"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win32.whl", hash = "sha256:6c4ca60fa24e85fe25b912b01e62cb969d69a23a5d5867682dd3e80b5b02581d"},
{file = "MarkupSafe-2.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b2f4bf27480f5e5e8ce285a8c8fd176c0b03e93dcc6646477d4630e83440c6a9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0717a7390a68be14b8c793ba258e075c6f4ca819f15edfc2a3a027c823718567"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:6557b31b5e2c9ddf0de32a691f2312a32f77cd7681d8af66c2692efdbef84c18"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:49e3ceeabbfb9d66c3aef5af3a60cc43b85c33df25ce03d0031a608b0a8b2e3f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:d7f9850398e85aba693bb640262d3611788b1f29a79f0c93c565694658f4071f"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:6a7fae0dd14cf60ad5ff42baa2e95727c3d81ded453457771d02b7d2b3f9c0c2"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:b7f2d075102dc8c794cbde1947378051c4e5180d52d276987b8d28a3bd58c17d"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e9936f0b261d4df76ad22f8fee3ae83b60d7c3e871292cd42f40b81b70afae85"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a7d351cbd8cfeb19ca00de495e224dea7e7d919659c2841bbb7f420ad03e2d6"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:60bf42e36abfaf9aff1f50f52644b336d4f0a3fd6d8a60ca0d054ac9f713a864"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d6c7ebd4e944c85e2c3421e612a7057a2f48d478d79e61800d81468a8d842207"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:f0567c4dc99f264f49fe27da5f735f414c4e7e7dd850cfd8e69f0862d7c74ea9"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:89c687013cb1cd489a0f0ac24febe8c7a666e6e221b783e53ac50ebf68e45d86"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win32.whl", hash = "sha256:a30e67a65b53ea0a5e62fe23682cfe22712e01f453b95233b25502f7c61cb415"},
{file = "MarkupSafe-2.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:611d1ad9a4288cf3e3c16014564df047fe08410e628f89805e475368bd304914"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5bb28c636d87e840583ee3adeb78172efc47c8b26127267f54a9c0ec251d41a9"},
{file = "MarkupSafe-2.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:be98f628055368795d818ebf93da628541e10b75b41c559fdf36d104c5787066"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1d609f577dc6e1aa17d746f8bd3c31aa4d258f4070d61b2aa5c4166c1539de35"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:7d91275b0245b1da4d4cfa07e0faedd5b0812efc15b702576d103293e252af1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:01a9b8ea66f1658938f65b93a85ebe8bc016e6769611be228d797c9d998dd298"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:47ab1e7b91c098ab893b828deafa1203de86d0bc6ab587b160f78fe6c4011f75"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:97383d78eb34da7e1fa37dd273c20ad4320929af65d156e35a5e2d89566d9dfb"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fcf051089389abe060c9cd7caa212c707e58153afa2c649f00346ce6d260f1b"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5855f8438a7d1d458206a2466bf82b0f104a3724bf96a1c781ab731e4201731a"},
{file = "MarkupSafe-2.0.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3dd007d54ee88b46be476e293f48c85048603f5f516008bee124ddd891398ed6"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aca6377c0cb8a8253e493c6b451565ac77e98c2951c45f913e0b52facdcff83f"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:04635854b943835a6ea959e948d19dcd311762c5c0c6e1f0e16ee57022669194"},
{file = "MarkupSafe-2.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6300b8454aa6930a24b9618fbb54b5a68135092bc666f7b06901f897fa5c2fee"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win32.whl", hash = "sha256:023cb26ec21ece8dc3907c0e8320058b2e0cb3c55cf9564da612bc325bed5e64"},
{file = "MarkupSafe-2.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:984d76483eb32f1bcb536dc27e4ad56bba4baa70be32fa87152832cdd9db0833"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:2ef54abee730b502252bcdf31b10dacb0a416229b72c18b19e24a4509f273d26"},
{file = "MarkupSafe-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3c112550557578c26af18a1ccc9e090bfe03832ae994343cfdacd287db6a6ae7"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_i686.whl", hash = "sha256:53edb4da6925ad13c07b6d26c2a852bd81e364f95301c66e930ab2aef5b5ddd8"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:f5653a225f31e113b152e56f154ccbe59eeb1c7487b39b9d9f9cdb58e6c79dc5"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:4efca8f86c54b22348a5467704e3fec767b2db12fc39c6d963168ab1d3fc9135"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:ab3ef638ace319fa26553db0624c4699e31a28bb2a835c5faca8f8acf6a5a902"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:f8ba0e8349a38d3001fae7eadded3f6606f0da5d748ee53cc1dab1d6527b9509"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c47adbc92fc1bb2b3274c4b3a43ae0e4573d9fbff4f54cd484555edbf030baf1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:37205cac2a79194e3750b0af2a5720d95f786a55ce7df90c3af697bfa100eaac"},
{file = "MarkupSafe-2.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1f2ade76b9903f39aa442b4aadd2177decb66525062db244b35d71d0ee8599b6"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4296f2b1ce8c86a6aea78613c34bb1a672ea0e3de9c6ba08a960efe0b0a09047"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f02365d4e99430a12647f09b6cc8bab61a6564363f313126f775eb4f6ef798e"},
{file = "MarkupSafe-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5b6d930f030f8ed98e3e6c98ffa0652bdb82601e7a016ec2ab5d7ff23baa78d1"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win32.whl", hash = "sha256:10f82115e21dc0dfec9ab5c0223652f7197feb168c940f3ef61563fc2d6beb74"},
{file = "MarkupSafe-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:693ce3f9e70a6cf7d2fb9e6c9d8b204b6b39897a2c4a1aa65728d5ac97dcc1d8"},
{file = "MarkupSafe-2.0.1.tar.gz", hash = "sha256:594c67807fb16238b30c44bdf74f36c02cdf22d1c8cda91ef8a0ed8dabf5620a"},
]
matplotlib-inline = [
{file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
{file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
@ -1129,8 +1309,8 @@ nonebot2 = [
{file = "nonebot2-2.0.0a16.tar.gz", hash = "sha256:f70475e0a9525ed22cc082e35b06145b005412f919880a862e72059a84b0d2d0"},
]
packaging = [
{file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"},
{file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"},
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
parso = [
{file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"},
@ -1238,9 +1418,13 @@ pygments = [
pygtrie = [
{file = "pygtrie-2.4.2.tar.gz", hash = "sha256:43205559d28863358dbbf25045029f58e2ab357317a59b11f11ade278ac64692"},
]
pyjwt = [
{file = "PyJWT-2.3.0-py3-none-any.whl", hash = "sha256:e0c4bb8d9f0af0c7f5b1ec4c5036309617d03d56932877f2f7a0beeb5318322f"},
{file = "PyJWT-2.3.0.tar.gz", hash = "sha256:b888b4d56f06f6dcd777210c334e69c737be74755d3e5e9ee3fe67dc18a0ee41"},
]
pyparsing = [
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
{file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"},
{file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"},
]
pyppeteer = [
{file = "pyppeteer-0.2.6-py3-none-any.whl", hash = "sha256:85adde940cc96820725db59cbdb13384aefd0dd043858cfa4f1c086c0f9e4137"},
@ -1258,6 +1442,14 @@ python-dotenv = [
{file = "python-dotenv-0.19.2.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"},
{file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"},
]
python-engineio = [
{file = "python-engineio-4.3.0.tar.gz", hash = "sha256:fed35eeacfa21f53f1fc05ef0cadd65a50780364da3a2be7650eb92f928fdb11"},
{file = "python_engineio-4.3.0-py3-none-any.whl", hash = "sha256:ad06a975f7e14cb3bb7137cbf70fd883804484d29acd58004d1db1e2a7fc0ad3"},
]
python-socketio = [
{file = "python-socketio-5.5.0.tar.gz", hash = "sha256:ce972ea1b82aa1811fa10d30cf0d5c251b9a1558c3d66829b6fe70854bcccf0b"},
{file = "python_socketio-5.5.0-py3-none-any.whl", hash = "sha256:ca28a0ff0ca5dd05ec5ba4ee2572fe06b96d6f0bc7df384d8b50fbbc06986134"},
]
pytz = [
{file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
{file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},

View File

@ -8,14 +8,18 @@ homepage = "https://github.com/felinae98/nonebot-bison"
keywords = ["nonebot", "nonebot2", "qqbot"]
readme = "README.md"
packages = [
{ include = "nonebot_bison/*.py", from = "./src/plugins/" },
{ include = "nonebot_bison/platform/*.py", from = "./src/plugins/" }
{ include = "nonebot_bison", from = "./src/plugins/" }
]
include = [
"src/plugins/nonebot_bison/admin_page/dist/**/*"
]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Development Status :: 3 - Alpha",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: Implementation :: CPython"
"Programming Language :: Python :: Implementation :: CPython",
"License :: OSI Approved :: MIT License"
]
[tool.poetry.dependencies]
@ -29,6 +33,10 @@ pyppeteer = "^0.2.5"
pillow = "^8.1.0"
nonebot-adapter-cqhttp = "^2.0.0-alpha.15"
apscheduler = "^3.7.0"
expiringdict = "^1.2.1"
pyjwt = "^2.1.0"
aiofiles = "^0.7.0"
python-socketio = "^5.4.0"
[tool.poetry.dev-dependencies]
ipdb = "^0.13.4"

View File

@ -8,3 +8,4 @@ from . import post
from . import platform
from . import types
from . import utils
from . import admin_page

View File

@ -0,0 +1,127 @@
from dataclasses import dataclass
from pathlib import Path
import os
from typing import Union
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from nonebot import get_driver, on_command
import nonebot
from nonebot.adapters.cqhttp.bot import Bot
from nonebot.adapters.cqhttp.event import GroupMessageEvent, PrivateMessageEvent
from nonebot.drivers.fastapi import Driver
from nonebot.log import logger
from nonebot.rule import to_me
from nonebot.typing import T_State
import socketio
import functools
from starlette.requests import Request
from .api import del_group_sub, test, get_global_conf, auth, get_subs_info, get_target_name, add_group_sub
from .token_manager import token_manager as tm
from .jwt import load_jwt
from ..plugin_config import plugin_config
URL_BASE = '/bison/'
GLOBAL_CONF_URL = f'{URL_BASE}api/global_conf'
AUTH_URL = f'{URL_BASE}api/auth'
SUBSCRIBE_URL = f'{URL_BASE}api/subs'
GET_TARGET_NAME_URL = f'{URL_BASE}api/target_name'
TEST_URL = f'{URL_BASE}test'
STATIC_PATH = (Path(__file__).parent / "dist").resolve()
sio = socketio.AsyncServer(async_mode="asgi", cors_allowed_origins="*")
socket_app = socketio.ASGIApp(sio, socketio_path="socket")
class SinglePageApplication(StaticFiles):
def __init__(self, directory: os.PathLike, index='index.html'):
self.index = index
super().__init__(directory=directory, packages=None, html=True, check_dir=True)
async def lookup_path(self, path: str) -> tuple[str, Union[os.stat_result, None]]:
full_path, stat_res = await super().lookup_path(path)
if stat_res is None:
return await super().lookup_path(self.index)
return (full_path, stat_res)
def register_router_fastapi(driver: Driver, socketio):
from fastapi.security import OAuth2PasswordBearer
from fastapi.param_functions import Depends
from fastapi import HTTPException, status
oath_scheme = OAuth2PasswordBearer(tokenUrl='token')
async def get_jwt_obj(token: str = Depends(oath_scheme)):
obj = load_jwt(token)
if not obj:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
return obj
async def check_group_permission(groupNumber: str, token_obj: dict = Depends(get_jwt_obj)):
groups = token_obj['groups']
for group in groups:
if int(groupNumber) == group['id']:
return
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
@dataclass
class AddSubscribeReq:
platformName: str
target: str
targetName: str
cats: list[str]
tags: list[str]
app = driver.server_app
static_path = STATIC_PATH
app.get(TEST_URL)(test)
app.get(GLOBAL_CONF_URL)(get_global_conf)
app.get(AUTH_URL)(auth)
@app.get(SUBSCRIBE_URL)
async def subs(jwt_obj: dict = Depends(get_jwt_obj)):
return await get_subs_info(jwt_obj)
@app.get(GET_TARGET_NAME_URL)
async def _get_target_name(platformName: str, target: str, jwt_obj: dict = Depends(get_jwt_obj)):
return await get_target_name(platformName, target, jwt_obj)
@app.post(SUBSCRIBE_URL, dependencies=[Depends(check_group_permission)])
async def _add_group_subs(groupNumber: str, req: AddSubscribeReq):
return await add_group_sub(group_number=groupNumber, platform_name=req.platformName,
target=req.target, target_name=req.targetName, cats=req.cats, tags=req.tags)
@app.delete(SUBSCRIBE_URL, dependencies=[Depends(check_group_permission)])
async def _del_group_subs(groupNumber: str, target: str, platformName: str):
return await del_group_sub(groupNumber, platformName, target)
app.mount(URL_BASE, SinglePageApplication(directory=static_path), name="bison")
def init():
driver = get_driver()
if driver.type == 'fastapi':
assert(isinstance(driver, Driver))
register_router_fastapi(driver, socket_app)
else:
logger.warning(f"Driver {driver.type} not supported")
return
host = str(driver.config.host)
port = driver.config.port
if host in ["0.0.0.0", "127.0.0.1"]:
host = "localhost"
logger.opt(colors=True).info(f"Nonebot test frontend will be running at: "
f"<b><u>http://{host}:{port}{URL_BASE}</u></b>")
if (STATIC_PATH / 'index.html').exists():
init()
get_token = on_command('后台管理', rule=to_me(), priority=5)
@get_token.handle()
async def send_token(bot: "Bot", event: PrivateMessageEvent, state: T_State):
driver = nonebot.get_driver()
token = tm.get_user_token((event.get_user_id(), event.sender.nickname))
await get_token.finish(f'请访问: {plugin_config.bison_outer_url}auth/{token}')
else:
logger.warning("Frontend file not found, please compile it or use docker or pypi version")

View File

@ -0,0 +1,102 @@
from ..platform import platform_manager, check_sub_target
from .token_manager import token_manager
from .jwt import pack_jwt
from ..config import Config, NoSuchSubscribeException, NoSuchUserException
import nonebot
from nonebot.adapters.cqhttp.bot import Bot
async def test():
return {"status": 200, "text": "test"}
async def get_global_conf():
res = {}
for platform_name, platform in platform_manager.items():
res[platform_name] = {
'platformName': platform_name,
'categories': platform.categories,
'enabledTag': platform.enable_tag,
'name': platform.name,
'hasTarget': getattr(platform, 'has_target')
}
return { 'platformConf': res }
async def get_admin_groups(qq: int):
bot = nonebot.get_bot()
groups = await bot.call_api('get_group_list')
res = []
for group in groups:
group_id = group['group_id']
users = await bot.call_api('get_group_member_list', group_id=group_id)
for user in users:
if user['user_id'] == qq and user['role'] in ('owner', 'admin'):
res.append({'id': group_id, 'name': group['group_name']})
return res
async def auth(token: str):
if qq_tuple := token_manager.get_user(token):
qq, nickname = qq_tuple
bot = nonebot.get_bot()
assert(isinstance(bot, Bot))
groups = await bot.call_api('get_group_list')
if str(qq) in nonebot.get_driver().config.superusers:
jwt_obj = {
'id': str(qq),
'groups': list(map(
lambda info: {'id': info['group_id'], 'name': info['group_name']},
groups)),
}
ret_obj = {
'type': 'admin',
'name': nickname,
'id': str(qq),
'token': pack_jwt(jwt_obj)
}
return { 'status': 200, **ret_obj }
if admin_groups := await get_admin_groups(int(qq)):
jwt_obj = {
'id': str(qq),
'groups': admin_groups
}
ret_obj = {
'type': 'user',
'name': nickname,
'id': str(qq),
'token': pack_jwt(jwt_obj)
}
return { 'status': 200, **ret_obj }
else:
return { 'status': 400, 'type': '', 'name': '', 'id': '', 'token': '' }
else:
return { 'status': 400, 'type': '', 'name': '', 'id': '', 'token': '' }
async def get_subs_info(jwt_obj: dict):
groups = jwt_obj['groups']
res = {}
for group in groups:
group_id = group['id']
config = Config()
subs = list(map(lambda sub: {
'platformName': sub['target_type'], 'target': sub['target'], 'targetName': sub['target_name'], 'cats': sub['cats'], 'tags': sub['tags']
}, config.list_subscribe(group_id, 'group')))
res[group_id] = {
'name': group['name'],
'subscribes': subs
}
return res
async def get_target_name(platform_name: str, target: str, jwt_obj: dict):
return {'targetName': await check_sub_target(platform_name, target)}
async def add_group_sub(group_number: str, platform_name: str, target: str,
target_name: str, cats: list[str], tags: list[str]):
config = Config()
config.add_subscribe(int(group_number), 'group', target, target_name, platform_name, cats, tags)
return { 'status': 200, 'msg': '' }
async def del_group_sub(group_number: str, platform_name: str, target: str):
config = Config()
try:
config.del_subscribe(int(group_number), 'group', target, platform_name)
except (NoSuchUserException, NoSuchSubscribeException):
return { 'status': 400, 'msg': '删除错误' }
return { 'status': 200, 'msg': '' }

View File

View File

@ -0,0 +1,20 @@
import random
import string
from typing import Optional
import jwt
import datetime
_key = ''.join(random.SystemRandom().choice(string.ascii_letters) for _ in range(16))
def pack_jwt(obj: dict) -> str:
return jwt.encode(
{'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1), **obj},
_key, algorithm='HS256'
)
def load_jwt(token: str) -> Optional[dict]:
try:
return jwt.decode(token, _key, algorithms=['HS256'])
except:
return None

View File

@ -0,0 +1,24 @@
from typing import Optional
from expiringdict import ExpiringDict
import random
import string
class TokenManager:
def __init__(self):
self.token_manager = ExpiringDict(max_len=100, max_age_seconds=60*10)
def get_user(self, token: str) -> Optional[tuple]:
res = self.token_manager.get(token)
assert(res is None or isinstance(res, tuple))
return res
def save_user(self, token: str, qq: tuple) -> None:
self.token_manager[token] = qq
def get_user_token(self, qq: tuple) -> str:
token = ''.join(random.choices(string.ascii_letters + string.digits, k=16))
self.save_user(token, qq)
return token
token_manager = TokenManager()

View File

@ -68,6 +68,9 @@ class Config(metaclass=Singleton):
if user_sub := self.user_target.get((query.user == user) & (query.user_type ==user_type)):
return user_sub['subs']
return []
def get_all_subscribe(self):
return self.user_target
def del_subscribe(self, user, user_type, target, target_type):
user_query = Query()

View File

@ -11,6 +11,7 @@ class PlugConfig(BaseSettings):
bison_browser: str = ''
bison_init_filter: bool = True
bison_use_queue: bool = True
bison_outer_url: str = 'http://localhost:8080/bison/'
class Config:
extra = 'ignore'