mirror of
https://github.com/suyiiyii/nonebot-bison.git
synced 2025-06-04 02:26:11 +08:00
Merge branch 'admin-page' into dev
This commit is contained in:
commit
514a1c4314
@ -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
1
.gitignore
vendored
@ -318,3 +318,4 @@ dist
|
||||
data*/*
|
||||
.env.*
|
||||
.vim/*
|
||||
!dist/.gitkeep
|
||||
|
@ -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
23
admin-frontend/.gitignore
vendored
Normal 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
46
admin-frontend/README.md
Normal 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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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/).
|
55
admin-frontend/package.json
Normal file
55
admin-frontend/package.json
Normal 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"
|
||||
}
|
||||
}
|
BIN
admin-frontend/public/favicon.ico
Normal file
BIN
admin-frontend/public/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
43
admin-frontend/public/index.html
Normal file
43
admin-frontend/public/index.html
Normal 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>
|
BIN
admin-frontend/public/logo192.png
Normal file
BIN
admin-frontend/public/logo192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.2 KiB |
BIN
admin-frontend/public/logo512.png
Normal file
BIN
admin-frontend/public/logo512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.4 KiB |
25
admin-frontend/public/manifest.json
Normal file
25
admin-frontend/public/manifest.json
Normal 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"
|
||||
}
|
3
admin-frontend/public/robots.txt
Normal file
3
admin-frontend/public/robots.txt
Normal file
@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
38
admin-frontend/src/App.css
Normal file
38
admin-frontend/src/App.css
Normal 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);
|
||||
}
|
||||
}
|
9
admin-frontend/src/App.test.tsx
Normal file
9
admin-frontend/src/App.test.tsx
Normal 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();
|
||||
});
|
60
admin-frontend/src/App.tsx
Normal file
60
admin-frontend/src/App.tsx
Normal 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;
|
33
admin-frontend/src/api/config.ts
Normal file
33
admin-frontend/src/api/config.ts
Normal 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;
|
||||
}
|
40
admin-frontend/src/api/utils.ts
Normal file
40
admin-frontend/src/api/utils.ts
Normal 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;
|
||||
});
|
117
admin-frontend/src/component/inputTag.tsx
Normal file
117
admin-frontend/src/component/inputTag.tsx
Normal 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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
}
|
94
admin-frontend/src/component/subscribeCard.tsx
Normal file
94
admin-frontend/src/component/subscribeCard.tsx
Normal 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>
|
||||
)
|
||||
}
|
13
admin-frontend/src/index.css
Normal file
13
admin-frontend/src/index.css
Normal 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;
|
||||
}
|
17
admin-frontend/src/index.tsx
Normal file
17
admin-frontend/src/index.tsx
Normal 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();
|
1
admin-frontend/src/logo.svg
Normal file
1
admin-frontend/src/logo.svg
Normal 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 |
5
admin-frontend/src/pages/admin/admin.css
Normal file
5
admin-frontend/src/pages/admin/admin.css
Normal file
@ -0,0 +1,5 @@
|
||||
.layout-side .user {
|
||||
height: 32px;
|
||||
margin: 16px;
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
}
|
150
admin-frontend/src/pages/admin/configPage/AddSubsModal.tsx
Normal file
150
admin-frontend/src/pages/admin/configPage/AddSubsModal.tsx
Normal 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>
|
||||
}
|
60
admin-frontend/src/pages/admin/configPage/index.tsx
Normal file
60
admin-frontend/src/pages/admin/configPage/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
36
admin-frontend/src/pages/admin/index.tsx
Normal file
36
admin-frontend/src/pages/admin/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
27
admin-frontend/src/pages/auth.tsx
Normal file
27
admin-frontend/src/pages/auth.tsx
Normal 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
1
admin-frontend/src/react-app-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
15
admin-frontend/src/reportWebVitals.ts
Normal file
15
admin-frontend/src/reportWebVitals.ts
Normal 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;
|
5
admin-frontend/src/setupTests.ts
Normal file
5
admin-frontend/src/setupTests.ts
Normal 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';
|
17
admin-frontend/src/utils/context.ts
Normal file
17
admin-frontend/src/utils/context.ts
Normal 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});
|
68
admin-frontend/src/utils/type.ts
Normal file
68
admin-frontend/src/utils/type.ts
Normal 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
|
||||
}
|
27
admin-frontend/tsconfig.json
Normal file
27
admin-frontend/tsconfig.json
Normal 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
12218
admin-frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
216
poetry.lock
generated
216
poetry.lock
generated
@ -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"},
|
||||
|
@ -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"
|
||||
|
@ -8,3 +8,4 @@ from . import post
|
||||
from . import platform
|
||||
from . import types
|
||||
from . import utils
|
||||
from . import admin_page
|
||||
|
127
src/plugins/nonebot_bison/admin_page/__init__.py
Normal file
127
src/plugins/nonebot_bison/admin_page/__init__.py
Normal 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")
|
102
src/plugins/nonebot_bison/admin_page/api.py
Normal file
102
src/plugins/nonebot_bison/admin_page/api.py
Normal 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': '' }
|
0
src/plugins/nonebot_bison/admin_page/dist/.gitkeep
vendored
Normal file
0
src/plugins/nonebot_bison/admin_page/dist/.gitkeep
vendored
Normal file
20
src/plugins/nonebot_bison/admin_page/jwt.py
Normal file
20
src/plugins/nonebot_bison/admin_page/jwt.py
Normal 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
|
||||
|
24
src/plugins/nonebot_bison/admin_page/token_manager.py
Normal file
24
src/plugins/nonebot_bison/admin_page/token_manager.py
Normal 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()
|
@ -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()
|
||||
|
@ -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'
|
||||
|
Loading…
x
Reference in New Issue
Block a user