mirror of
https://github.com/suyiiyii/nonebot-bison.git
synced 2026-06-23 22:16:53 +08:00
Merge branch 'admin-page' into dev
This commit is contained in:
+30
-1
@@ -6,6 +6,7 @@ orbs:
|
|||||||
# so you dont have to copy and paste it everywhere.
|
# so you dont have to copy and paste it everywhere.
|
||||||
# See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/python
|
# See the orb documentation here: https://circleci.com/developer/orbs/orb/circleci/python
|
||||||
python: circleci/python@1.4
|
python: circleci/python@1.4
|
||||||
|
node: circleci/node@4.7.0
|
||||||
# poetry: frameio/poetry@0.21.0
|
# poetry: frameio/poetry@0.21.0
|
||||||
swissknife: roopakv/swissknife@0.59.0
|
swissknife: roopakv/swissknife@0.59.0
|
||||||
docker: circleci/docker@1.7.0
|
docker: circleci/docker@1.7.0
|
||||||
@@ -13,7 +14,13 @@ orbs:
|
|||||||
workflows:
|
workflows:
|
||||||
build-test-publish:
|
build-test-publish:
|
||||||
jobs:
|
jobs:
|
||||||
|
- build-frontend:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
- test:
|
- test:
|
||||||
|
requires:
|
||||||
|
- build-frontend
|
||||||
filters:
|
filters:
|
||||||
tags:
|
tags:
|
||||||
only: /.*/
|
only: /.*/
|
||||||
@@ -25,7 +32,7 @@ workflows:
|
|||||||
ignore: /.*/
|
ignore: /.*/
|
||||||
tags:
|
tags:
|
||||||
only: /^v.*/
|
only: /^v.*/
|
||||||
- docker/publish:
|
- docker/publish: &docker-push
|
||||||
requires:
|
requires:
|
||||||
- test
|
- test
|
||||||
filters:
|
filters:
|
||||||
@@ -40,8 +47,30 @@ workflows:
|
|||||||
update-description: true
|
update-description: true
|
||||||
docker-username: DOCKERHUB_USERNAME
|
docker-username: DOCKERHUB_USERNAME
|
||||||
docker-password: DOCKERHUB_PASSWORD
|
docker-password: DOCKERHUB_PASSWORD
|
||||||
|
- docker/publish:
|
||||||
|
<<: *docker-push
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
ignore: /.*/
|
||||||
|
tag: ${CIRCLE_BRANCH}
|
||||||
|
|
||||||
jobs:
|
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:
|
test:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/python:3.9
|
- image: cimg/python:3.9
|
||||||
|
|||||||
@@ -318,3 +318,4 @@ dist
|
|||||||
data*/*
|
data*/*
|
||||||
.env.*
|
.env.*
|
||||||
.vim/*
|
.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
|
FROM python:3.9
|
||||||
RUN python3 -m pip install poetry && poetry config virtualenvs.create false
|
RUN python3 -m pip install poetry && poetry config virtualenvs.create false
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -6,5 +11,6 @@ RUN poetry install --no-root --no-dev
|
|||||||
# RUN PYPPETEER_DOWNLOAD_HOST='http://npm.taobao.org/mirrors' pyppeteer-install
|
# RUN PYPPETEER_DOWNLOAD_HOST='http://npm.taobao.org/mirrors' pyppeteer-install
|
||||||
ADD src /app/src
|
ADD src /app/src
|
||||||
ADD bot.py /app/
|
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
|
ENV HOST=0.0.0.0
|
||||||
CMD ["python", "bot.py"]
|
CMD ["python", "bot.py"]
|
||||||
|
|||||||
@@ -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*
|
||||||
@@ -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/).
|
||||||
@@ -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 |
@@ -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 |
@@ -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"
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
@@ -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;
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -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 |
@@ -0,0 +1,5 @@
|
|||||||
|
.layout-side .user {
|
||||||
|
height: 32px;
|
||||||
|
margin: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -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
@@ -0,0 +1 @@
|
|||||||
|
/// <reference types="react-scripts" />
|
||||||
@@ -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;
|
||||||
@@ -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';
|
||||||
@@ -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});
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Generated
+204
-12
@@ -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]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "3.3.4"
|
version = "3.3.4"
|
||||||
@@ -113,6 +121,14 @@ soupsieve = ">1.2"
|
|||||||
html5lib = ["html5lib"]
|
html5lib = ["html5lib"]
|
||||||
lxml = ["lxml"]
|
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]]
|
[[package]]
|
||||||
name = "bs4"
|
name = "bs4"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
@@ -181,6 +197,17 @@ category = "dev"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
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]]
|
[[package]]
|
||||||
name = "fastapi"
|
name = "fastapi"
|
||||||
version = "0.68.2"
|
version = "0.68.2"
|
||||||
@@ -240,7 +267,7 @@ python-versions = ">=3.6.1"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpcore"
|
name = "httpcore"
|
||||||
version = "0.14.2"
|
version = "0.14.3"
|
||||||
description = "A minimal low-level HTTP client."
|
description = "A minimal low-level HTTP client."
|
||||||
category = "main"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -387,6 +414,20 @@ parso = ">=0.8.0,<0.9.0"
|
|||||||
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
|
qa = ["flake8 (==3.8.3)", "mypy (==0.782)"]
|
||||||
testing = ["Django (<3.1)", "colorama", "docopt", "pytest (<7.0.0)"]
|
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]]
|
[[package]]
|
||||||
name = "loguru"
|
name = "loguru"
|
||||||
version = "0.5.3"
|
version = "0.5.3"
|
||||||
@@ -402,6 +443,14 @@ win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""}
|
|||||||
[package.extras]
|
[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)"]
|
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]]
|
[[package]]
|
||||||
name = "matplotlib-inline"
|
name = "matplotlib-inline"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
@@ -450,14 +499,14 @@ aiohttp = ["aiohttp[speedups] (>=3.7.4,<4.0.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "21.2"
|
version = "21.3"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pyparsing = ">=2.0.2,<3"
|
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parso"
|
name = "parso"
|
||||||
@@ -577,13 +626,30 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
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]]
|
[[package]]
|
||||||
name = "pyparsing"
|
name = "pyparsing"
|
||||||
version = "2.4.7"
|
version = "3.0.6"
|
||||||
description = "Python parsing module"
|
description = "Python parsing module"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
diagrams = ["jinja2", "railroad-diagrams"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyppeteer"
|
name = "pyppeteer"
|
||||||
@@ -647,6 +713,34 @@ python-versions = ">=3.5"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
cli = ["click (>=5.0)"]
|
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]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2021.3"
|
version = "2021.3"
|
||||||
@@ -924,9 +1018,13 @@ testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytes
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.9"
|
python-versions = "^3.9"
|
||||||
content-hash = "5e71b794d68042efbfb810bfd56d652cf5439fe02f6bb02ed31142ecb0e5adc7"
|
content-hash = "77b95ff5c357cba44f6102ef29a07e59fa5b6a2abc814e6f97c41c9531f597ab"
|
||||||
|
|
||||||
[metadata.files]
|
[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 = [
|
anyio = [
|
||||||
{file = "anyio-3.3.4-py3-none-any.whl", hash = "sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66"},
|
{file = "anyio-3.3.4-py3-none-any.whl", hash = "sha256:4fd09a25ab7fa01d34512b7249e366cd10358cdafc95022c7ff8c8f8a5026d66"},
|
||||||
{file = "anyio-3.3.4.tar.gz", hash = "sha256:67da67b5b21f96b9d3d65daa6ea99f5d5282cb09f50eb4456f8fb51dffefc3ff"},
|
{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-py3-none-any.whl", hash = "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf"},
|
||||||
{file = "beautifulsoup4-4.10.0.tar.gz", hash = "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891"},
|
{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 = [
|
bs4 = [
|
||||||
{file = "bs4-0.0.1.tar.gz", hash = "sha256:36ecea1fd7cc5c0c6e4a1ff075df26d50da647b75376626cc186e2212886dd3a"},
|
{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-py3-none-any.whl", hash = "sha256:7b12e7c3c6ab203a29e157335e9122cb03de9ab7264b137594103fd4a683b374"},
|
||||||
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
|
{file = "decorator-5.1.0.tar.gz", hash = "sha256:e59913af105b9860aa2c8d3272d9de5a56a4e608db9a2f167a8480b323d529a7"},
|
||||||
]
|
]
|
||||||
|
expiringdict = [
|
||||||
|
{file = "expiringdict-1.2.1.tar.gz", hash = "sha256:fe2ba427220425c3c8a3d29f6d2e2985bcee323f8bcd4021e68ebefbd90d8250"},
|
||||||
|
]
|
||||||
fastapi = [
|
fastapi = [
|
||||||
{file = "fastapi-0.68.2-py3-none-any.whl", hash = "sha256:36bcdd3dbea87c586061005e4a40b9bd0145afd766655b4e0ec1d8870b32555c"},
|
{file = "fastapi-0.68.2-py3-none-any.whl", hash = "sha256:36bcdd3dbea87c586061005e4a40b9bd0145afd766655b4e0ec1d8870b32555c"},
|
||||||
{file = "fastapi-0.68.2.tar.gz", hash = "sha256:38526fc46bda73f7ec92033952677323c16061e70a91d15c95f18b11895da494"},
|
{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"},
|
{file = "hpack-4.0.0.tar.gz", hash = "sha256:fc41de0c63e687ebffde81187a948221294896f6bdc0ae2312708df339430095"},
|
||||||
]
|
]
|
||||||
httpcore = [
|
httpcore = [
|
||||||
{file = "httpcore-0.14.2-py3-none-any.whl", hash = "sha256:47d7c8f755719d4a57be0b6e022897e9e963bf9ce4b15b9cc006a38a1cfa2932"},
|
{file = "httpcore-0.14.3-py3-none-any.whl", hash = "sha256:9a98d2416b78976fc5396ff1f6b26ae9885efbb3105d24eed490f20ab4c95ec1"},
|
||||||
{file = "httpcore-0.14.2.tar.gz", hash = "sha256:ff8f8b9434ec4823f95a30596fbe78039913e706d3e598b0b8955b1e1828e093"},
|
{file = "httpcore-0.14.3.tar.gz", hash = "sha256:d10162a63265a0228d5807964bd964478cbdb5178f9a2eedfebb2faba27eef5d"},
|
||||||
]
|
]
|
||||||
httptools = [
|
httptools = [
|
||||||
{file = "httptools-0.2.0-cp35-cp35m-macosx_10_14_x86_64.whl", hash = "sha256:79dbc21f3612a78b28384e989b21872e2e3cf3968532601544696e4ed0007ce5"},
|
{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-py2.py3-none-any.whl", hash = "sha256:637c9635fcf47945ceb91cd7f320234a7be540ded6f3e99a50cb6febdfd1ba8d"},
|
||||||
{file = "jedi-0.18.1.tar.gz", hash = "sha256:74137626a64a99c8eb6ae5832d99b3bdd7d29a3850fe2aa80a4126b2a7d949ab"},
|
{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 = [
|
loguru = [
|
||||||
{file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"},
|
{file = "loguru-0.5.3-py3-none-any.whl", hash = "sha256:f8087ac396b5ee5f67c963b495d615ebbceac2796379599820e324419d53667c"},
|
||||||
{file = "loguru-0.5.3.tar.gz", hash = "sha256:b28e72ac7a98be3d28ad28570299a393dfcd32e5e3f6a353dec94675767b6319"},
|
{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 = [
|
matplotlib-inline = [
|
||||||
{file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
|
{file = "matplotlib-inline-0.1.3.tar.gz", hash = "sha256:a04bfba22e0d1395479f866853ec1ee28eea1485c1d69a6faf00dc3e24ff34ee"},
|
||||||
{file = "matplotlib_inline-0.1.3-py3-none-any.whl", hash = "sha256:aed605ba3b72462d64d475a21a9296f400a19c4f74a31b59103d2a99ffd5aa5c"},
|
{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"},
|
{file = "nonebot2-2.0.0a16.tar.gz", hash = "sha256:f70475e0a9525ed22cc082e35b06145b005412f919880a862e72059a84b0d2d0"},
|
||||||
]
|
]
|
||||||
packaging = [
|
packaging = [
|
||||||
{file = "packaging-21.2-py3-none-any.whl", hash = "sha256:14317396d1e8cdb122989b916fa2c7e9ca8e2be9e8060a6eff75b6b7b4d8a7e0"},
|
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||||
{file = "packaging-21.2.tar.gz", hash = "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966"},
|
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||||
]
|
]
|
||||||
parso = [
|
parso = [
|
||||||
{file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"},
|
{file = "parso-0.8.2-py2.py3-none-any.whl", hash = "sha256:a8c4922db71e4fdb90e0d0bc6e50f9b273d3397925e5e60a717e719201778d22"},
|
||||||
@@ -1238,9 +1418,13 @@ pygments = [
|
|||||||
pygtrie = [
|
pygtrie = [
|
||||||
{file = "pygtrie-2.4.2.tar.gz", hash = "sha256:43205559d28863358dbbf25045029f58e2ab357317a59b11f11ade278ac64692"},
|
{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 = [
|
pyparsing = [
|
||||||
{file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"},
|
{file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"},
|
||||||
{file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"},
|
{file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"},
|
||||||
]
|
]
|
||||||
pyppeteer = [
|
pyppeteer = [
|
||||||
{file = "pyppeteer-0.2.6-py3-none-any.whl", hash = "sha256:85adde940cc96820725db59cbdb13384aefd0dd043858cfa4f1c086c0f9e4137"},
|
{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.tar.gz", hash = "sha256:a5de49a31e953b45ff2d2fd434bbc2670e8db5273606c1e737cc6b93eff3655f"},
|
||||||
{file = "python_dotenv-0.19.2-py2.py3-none-any.whl", hash = "sha256:32b2bdc1873fd3a3c346da1c6db83d0053c3c62f28f1f38516070c4c8971b1d3"},
|
{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 = [
|
pytz = [
|
||||||
{file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
|
{file = "pytz-2021.3-py2.py3-none-any.whl", hash = "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c"},
|
||||||
{file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},
|
{file = "pytz-2021.3.tar.gz", hash = "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"},
|
||||||
|
|||||||
+12
-4
@@ -8,14 +8,18 @@ homepage = "https://github.com/felinae98/nonebot-bison"
|
|||||||
keywords = ["nonebot", "nonebot2", "qqbot"]
|
keywords = ["nonebot", "nonebot2", "qqbot"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
packages = [
|
packages = [
|
||||||
{ include = "nonebot_bison/*.py", from = "./src/plugins/" },
|
{ include = "nonebot_bison", from = "./src/plugins/" }
|
||||||
{ include = "nonebot_bison/platform/*.py", from = "./src/plugins/" }
|
]
|
||||||
|
include = [
|
||||||
|
"src/plugins/nonebot_bison/admin_page/dist/**/*"
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 2 - Pre-Alpha",
|
"Development Status :: 3 - Alpha",
|
||||||
"Operating System :: POSIX :: Linux",
|
"Operating System :: POSIX :: Linux",
|
||||||
|
"Operating System :: Microsoft :: Windows",
|
||||||
"Programming Language :: Python :: 3.9",
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: Implementation :: CPython"
|
"Programming Language :: Python :: Implementation :: CPython",
|
||||||
|
"License :: OSI Approved :: MIT License"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
@@ -29,6 +33,10 @@ pyppeteer = "^0.2.5"
|
|||||||
pillow = "^8.1.0"
|
pillow = "^8.1.0"
|
||||||
nonebot-adapter-cqhttp = "^2.0.0-alpha.15"
|
nonebot-adapter-cqhttp = "^2.0.0-alpha.15"
|
||||||
apscheduler = "^3.7.0"
|
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]
|
[tool.poetry.dev-dependencies]
|
||||||
ipdb = "^0.13.4"
|
ipdb = "^0.13.4"
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ from . import post
|
|||||||
from . import platform
|
from . import platform
|
||||||
from . import types
|
from . import types
|
||||||
from . import utils
|
from . import utils
|
||||||
|
from . import admin_page
|
||||||
|
|||||||
@@ -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")
|
||||||
@@ -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,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
|
||||||
|
|
||||||
@@ -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()
|
||||||
@@ -69,6 +69,9 @@ class Config(metaclass=Singleton):
|
|||||||
return user_sub['subs']
|
return user_sub['subs']
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
def get_all_subscribe(self):
|
||||||
|
return self.user_target
|
||||||
|
|
||||||
def del_subscribe(self, user, user_type, target, target_type):
|
def del_subscribe(self, user, user_type, target, target_type):
|
||||||
user_query = Query()
|
user_query = Query()
|
||||||
query = (user_query.user == user) & (user_query.user_type == user_type)
|
query = (user_query.user == user) & (user_query.user_type == user_type)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ class PlugConfig(BaseSettings):
|
|||||||
bison_browser: str = ''
|
bison_browser: str = ''
|
||||||
bison_init_filter: bool = True
|
bison_init_filter: bool = True
|
||||||
bison_use_queue: bool = True
|
bison_use_queue: bool = True
|
||||||
|
bison_outer_url: str = 'http://localhost:8080/bison/'
|
||||||
|
|
||||||
class Config:
|
class Config:
|
||||||
extra = 'ignore'
|
extra = 'ignore'
|
||||||
|
|||||||
Reference in New Issue
Block a user