Merge branch 'admin-page' into dev

This commit is contained in:
felinae98
2021-11-26 14:59:45 +08:00
44 changed files with 13777 additions and 17 deletions
+1
View File
@@ -8,3 +8,4 @@ from . import post
from . import platform
from . import types
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")
+102
View File
@@ -0,0 +1,102 @@
from ..platform import platform_manager, check_sub_target
from .token_manager import token_manager
from .jwt import pack_jwt
from ..config import Config, NoSuchSubscribeException, NoSuchUserException
import nonebot
from nonebot.adapters.cqhttp.bot import Bot
async def test():
return {"status": 200, "text": "test"}
async def get_global_conf():
res = {}
for platform_name, platform in platform_manager.items():
res[platform_name] = {
'platformName': platform_name,
'categories': platform.categories,
'enabledTag': platform.enable_tag,
'name': platform.name,
'hasTarget': getattr(platform, 'has_target')
}
return { 'platformConf': res }
async def get_admin_groups(qq: int):
bot = nonebot.get_bot()
groups = await bot.call_api('get_group_list')
res = []
for group in groups:
group_id = group['group_id']
users = await bot.call_api('get_group_member_list', group_id=group_id)
for user in users:
if user['user_id'] == qq and user['role'] in ('owner', 'admin'):
res.append({'id': group_id, 'name': group['group_name']})
return res
async def auth(token: str):
if qq_tuple := token_manager.get_user(token):
qq, nickname = qq_tuple
bot = nonebot.get_bot()
assert(isinstance(bot, Bot))
groups = await bot.call_api('get_group_list')
if str(qq) in nonebot.get_driver().config.superusers:
jwt_obj = {
'id': str(qq),
'groups': list(map(
lambda info: {'id': info['group_id'], 'name': info['group_name']},
groups)),
}
ret_obj = {
'type': 'admin',
'name': nickname,
'id': str(qq),
'token': pack_jwt(jwt_obj)
}
return { 'status': 200, **ret_obj }
if admin_groups := await get_admin_groups(int(qq)):
jwt_obj = {
'id': str(qq),
'groups': admin_groups
}
ret_obj = {
'type': 'user',
'name': nickname,
'id': str(qq),
'token': pack_jwt(jwt_obj)
}
return { 'status': 200, **ret_obj }
else:
return { 'status': 400, 'type': '', 'name': '', 'id': '', 'token': '' }
else:
return { 'status': 400, 'type': '', 'name': '', 'id': '', 'token': '' }
async def get_subs_info(jwt_obj: dict):
groups = jwt_obj['groups']
res = {}
for group in groups:
group_id = group['id']
config = Config()
subs = list(map(lambda sub: {
'platformName': sub['target_type'], 'target': sub['target'], 'targetName': sub['target_name'], 'cats': sub['cats'], 'tags': sub['tags']
}, config.list_subscribe(group_id, 'group')))
res[group_id] = {
'name': group['name'],
'subscribes': subs
}
return res
async def get_target_name(platform_name: str, target: str, jwt_obj: dict):
return {'targetName': await check_sub_target(platform_name, target)}
async def add_group_sub(group_number: str, platform_name: str, target: str,
target_name: str, cats: list[str], tags: list[str]):
config = Config()
config.add_subscribe(int(group_number), 'group', target, target_name, platform_name, cats, tags)
return { 'status': 200, 'msg': '' }
async def del_group_sub(group_number: str, platform_name: str, target: str):
config = Config()
try:
config.del_subscribe(int(group_number), 'group', target, platform_name)
except (NoSuchUserException, NoSuchSubscribeException):
return { 'status': 400, 'msg': '删除错误' }
return { 'status': 200, 'msg': '' }
@@ -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()
+3
View File
@@ -68,6 +68,9 @@ class Config(metaclass=Singleton):
if user_sub := self.user_target.get((query.user == user) & (query.user_type ==user_type)):
return user_sub['subs']
return []
def get_all_subscribe(self):
return self.user_target
def del_subscribe(self, user, user_type, target, target_type):
user_query = Query()
@@ -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'