🚚 修改 nonebot_bison 项目结构 (#211)

* 🎨 修改 nonebot_bison 目录位置

* auto fix by pre-commit hooks

* 🚚 fix frontend build target

* 🚚 use soft link

* Revert "🚚 use soft link"

This reverts commit de21f79d5ae1bd5515b04f42a4138cb25ddf3e62.

* 🚚 modify dockerfile

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: felinae98 <731499577@qq.com>
This commit is contained in:
uy/sun
2023-03-09 17:32:51 +08:00
committed by GitHub
parent 3082587662
commit 90816796c7
65 changed files with 83 additions and 35 deletions
+85
View File
@@ -0,0 +1,85 @@
import os
from pathlib import Path
from typing import Union
import socketio
from fastapi.applications import FastAPI
from fastapi.staticfiles import StaticFiles
from nonebot import get_driver, on_command
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.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
from ..plugin_config import plugin_config
from .api import router as api_router
from .token_manager import token_manager as tm
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)
def lookup_path(self, path: str) -> tuple[str, Union[os.stat_result, None]]:
full_path, stat_res = super().lookup_path(path)
if stat_res is None:
return super().lookup_path(self.index)
return (full_path, stat_res)
def register_router_fastapi(driver: Driver, socketio):
static_path = STATIC_PATH
nonebot_app = FastAPI(
title="nonebot-bison",
description="nonebot-bison webui and api",
)
nonebot_app.include_router(api_router)
nonebot_app.mount(
"/", SinglePageApplication(directory=static_path), name="bison-frontend"
)
app = driver.server_app
app.mount("/bison", nonebot_app, "nonebot-bison")
def init():
driver = get_driver()
if 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}/bison</u></b>"
)
if (STATIC_PATH / "index.html").exists():
init()
get_token = on_command("后台管理", rule=to_me(), priority=5, aliases={"管理后台"})
@get_token.handle()
async def send_token(bot: "Bot", event: PrivateMessageEvent, state: T_State):
token = tm.get_user_token((event.get_user_id(), event.sender.nickname))
await get_token.finish(f"请访问: {plugin_config.bison_outer_url}auth/{token}")
get_token.__help__name__ = "获取后台管理地址"
get_token.__help__info__ = "获取管理bot后台的地址,该地址会" "在一段时间过后过期,请不要泄漏该地址"
else:
logger.warning(
"Frontend file not found, please compile it or use docker or pypi version"
)
+214
View File
@@ -0,0 +1,214 @@
import nonebot
from fastapi import status
from fastapi.exceptions import HTTPException
from fastapi.param_functions import Depends
from fastapi.routing import APIRouter
from fastapi.security.oauth2 import OAuth2PasswordBearer
from ..apis import check_sub_target
from ..config import (
NoSuchSubscribeException,
NoSuchTargetException,
NoSuchUserException,
config,
)
from ..config.db_config import SubscribeDupException
from ..platform import platform_manager
from ..types import Target as T_Target
from ..types import User, WeightConfig
from ..utils.get_bot import get_bot, get_groups
from .jwt import load_jwt, pack_jwt
from .token_manager import token_manager
from .types import (
AddSubscribeReq,
GlobalConf,
PlatformConfig,
StatusResp,
SubscribeConfig,
SubscribeGroupDetail,
SubscribeResp,
TokenResp,
)
router = APIRouter(prefix="/api", tags=["api"])
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: int, 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)
async def check_is_superuser(token_obj: dict = Depends(get_jwt_obj)):
if token_obj.get("type") != "admin":
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN)
@router.get("/global_conf")
async def get_global_conf() -> GlobalConf:
res = {}
for platform_name, platform in platform_manager.items():
res[platform_name] = PlatformConfig(
platformName=platform_name,
categories=platform.categories,
enabledTag=platform.enable_tag,
name=platform.name,
hasTarget=getattr(platform, "has_target"),
)
return GlobalConf(platformConf=res)
async def get_admin_groups(qq: int):
res = []
for group in await get_groups():
group_id = group["group_id"]
bot = get_bot(User(group_id, "group"))
if not bot:
continue
users = await bot.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
@router.get("/auth")
async def auth(token: str) -> TokenResp:
if qq_tuple := token_manager.get_user(token):
qq, nickname = qq_tuple
if str(qq) in nonebot.get_driver().config.superusers:
jwt_obj = {
"id": qq,
"type": "admin",
"groups": list(
map(
lambda info: {
"id": info["group_id"],
"name": info["group_name"],
},
await get_groups(),
)
),
}
ret_obj = TokenResp(
type="admin",
name=nickname,
id=qq,
token=pack_jwt(jwt_obj),
)
return ret_obj
if admin_groups := await get_admin_groups(int(qq)):
jwt_obj = {"id": str(qq), "type": "user", "groups": admin_groups}
ret_obj = TokenResp(
type="user",
name=nickname,
id=qq,
token=pack_jwt(jwt_obj),
)
return ret_obj
else:
raise HTTPException(400, "permission denied")
else:
raise HTTPException(400, "code error")
@router.get("/subs")
async def get_subs_info(jwt_obj: dict = Depends(get_jwt_obj)) -> SubscribeResp:
groups = jwt_obj["groups"]
res: SubscribeResp = {}
for group in groups:
group_id = group["id"]
raw_subs = await config.list_subscribe(group_id, "group")
subs = list(
map(
lambda sub: SubscribeConfig(
platformName=sub.target.platform_name,
targetName=sub.target.target_name,
cats=sub.categories, # type: ignore
tags=sub.tags, # type: ignore
target=sub.target.target,
),
raw_subs,
)
)
res[group_id] = SubscribeGroupDetail(name=group["name"], subscribes=subs)
return res
@router.get("/target_name", dependencies=[Depends(get_jwt_obj)])
async def get_target_name(platformName: str, target: str):
return {"targetName": await check_sub_target(platformName, T_Target(target))}
@router.post("/subs", dependencies=[Depends(check_group_permission)])
async def add_group_sub(groupNumber: int, req: AddSubscribeReq) -> StatusResp:
try:
await config.add_subscribe(
int(groupNumber),
"group",
T_Target(req.target),
req.targetName,
req.platformName,
req.cats,
req.tags,
)
return StatusResp(ok=True, msg="")
except SubscribeDupException:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "subscribe duplicated")
@router.delete("/subs", dependencies=[Depends(check_group_permission)])
async def del_group_sub(groupNumber: int, platformName: str, target: str):
try:
await config.del_subscribe(int(groupNumber), "group", target, platformName)
except (NoSuchUserException, NoSuchSubscribeException):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "no such user or subscribe")
return StatusResp(ok=True, msg="")
@router.patch("/subs", dependencies=[Depends(check_group_permission)])
async def update_group_sub(groupNumber: int, req: AddSubscribeReq):
try:
await config.update_subscribe(
int(groupNumber),
"group",
req.target,
req.targetName,
req.platformName,
req.cats,
req.tags,
)
except (NoSuchUserException, NoSuchSubscribeException):
raise HTTPException(status.HTTP_400_BAD_REQUEST, "no such user or subscribe")
return StatusResp(ok=True, msg="")
@router.get("/weight", dependencies=[Depends(check_is_superuser)])
async def get_weight_config():
return await config.get_all_weight_config()
@router.put("/weight", dependencies=[Depends(check_is_superuser)])
async def update_weigth_config(
platformName: str, target: str, weight_config: WeightConfig
):
try:
await config.update_time_weight_config(
T_Target(target), platformName, weight_config
)
except NoSuchTargetException:
raise HTTPException(status.HTTP_400_BAD_REQUEST, "no such subscribe")
return StatusResp(ok=True, msg="")
View File
+23
View File
@@ -0,0 +1,23 @@
import datetime
import random
import string
from typing import Optional
import jwt
_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
+26
View File
@@ -0,0 +1,26 @@
import random
import string
from typing import Optional
from expiringdict import ExpiringDict
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()
+52
View File
@@ -0,0 +1,52 @@
from pydantic import BaseModel
class PlatformConfig(BaseModel):
name: str
categories: dict[int, str]
enabledTag: bool
platformName: str
hasTarget: bool
AllPlatformConf = dict[str, PlatformConfig]
class GlobalConf(BaseModel):
platformConf: AllPlatformConf
class TokenResp(BaseModel):
token: str
type: str
id: int
name: str
class SubscribeConfig(BaseModel):
platformName: str
target: str
targetName: str
cats: list[int]
tags: list[str]
class SubscribeGroupDetail(BaseModel):
name: str
subscribes: list[SubscribeConfig]
SubscribeResp = dict[str, SubscribeGroupDetail]
class AddSubscribeReq(BaseModel):
platformName: str
target: str
targetName: str
cats: list[int]
tags: list[str]
class StatusResp(BaseModel):
ok: bool
msg: str