Skip to content

Commit

Permalink
🚧 修改插件加载方式
Browse files Browse the repository at this point in the history
  • Loading branch information
zhulinyv committed Apr 10, 2024
1 parent 95a6e10 commit 8ec7b36
Show file tree
Hide file tree
Showing 88 changed files with 6,188 additions and 49 deletions.
17 changes: 16 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,25 @@ win32-setctime = "1.1.0"
wordcloud = "1.8.2.2"

[tool.nonebot]
plugins = ["nonebot_plugin_guild_patch", "nonebot_bison"]
plugins = ["nonebot_plugin_guild_patch"]
plugin_dirs = ["src/plugins"]
adapters = [{name = "OneBot V11", module_name = "nonebot.adapters.onebot.v11", project_link = "nonebot-adapter-onebot", desc = "OneBot V11 协议"}]

# https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html
[tool.black]
line-length = 500

# https://beta.ruff.rs/docs/settings/
[tool.ruff]
line-length = 500
# https://beta.ruff.rs/docs/rules/
select = ["E", "W", "F"]
ignore = ["F401"]
# Exclude a variety of commonly ignored directories.
respect-gitignore = true
ignore-init-module-imports = true


[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"
47 changes: 47 additions & 0 deletions src/plugins/nonebot_bison/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from nonebot.plugin import PluginMetadata, require

require("nonebot_plugin_apscheduler")
require("nonebot_plugin_datastore")
require("nonebot_plugin_saa")

import nonebot_plugin_saa

from .plugin_config import PlugConfig, plugin_config
from . import post, send, theme, types, utils, config, platform, bootstrap, scheduler, admin_page, sub_manager

__help__version__ = "0.8.2"
nonebot_plugin_saa.enable_auto_select_bot()

__help__plugin__name__ = "nonebot_bison"
__usage__ = (
"本bot可以提供b站、微博等社交媒体的消息订阅,详情请查看本bot文档,"
f"或者{'at本bot' if plugin_config.bison_to_me else '' }发送“添加订阅”订阅第一个帐号,"
"发送“查询订阅”或“删除订阅”管理订阅"
)

__supported_adapters__ = nonebot_plugin_saa.__plugin_meta__.supported_adapters

__plugin_meta__ = PluginMetadata(
name="Bison",
description="通用订阅推送插件",
usage=__usage__,
type="application",
homepage="https://github.com/felinae98/nonebot-bison",
config=PlugConfig,
supported_adapters=__supported_adapters__,
extra={"version": __help__version__, "docs": "https://nonebot-bison.netlify.app/"},
)

__all__ = [
"admin_page",
"bootstrap",
"config",
"sub_manager",
"post",
"scheduler",
"send",
"platform",
"types",
"utils",
"theme",
]
94 changes: 94 additions & 0 deletions src/plugins/nonebot_bison/admin_page/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import os
from pathlib import Path
from typing import TYPE_CHECKING

from nonebot.log import logger
from nonebot.rule import to_me
from nonebot.typing import T_State
from nonebot import get_driver, on_command
from nonebot.adapters.onebot.v11 import Bot
from nonebot.adapters.onebot.v11.event import PrivateMessageEvent

from .api import router as api_router
from ..plugin_config import plugin_config
from .token_manager import token_manager as tm

if TYPE_CHECKING:
from nonebot.drivers.fastapi import Driver


STATIC_PATH = (Path(__file__).parent / "dist").resolve()


def init_fastapi(driver: "Driver"):
import socketio
from fastapi.applications import FastAPI
from fastapi.staticfiles import StaticFiles

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, 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")

register_router_fastapi(driver, socket_app)
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 Bison frontend will be running at: <b><u>http://{host}:{port}/bison</u></b>")
logger.opt(colors=True).info("该页面不能被直接访问,请私聊bot <b><u>后台管理</u></b> 以获取可访问地址")


def register_get_token_handler():
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.outer_url / 'auth' / token}")

get_token.__help__name__ = "获取后台管理地址" # type: ignore
get_token.__help__info__ = "获取管理bot后台的地址,该地址会在一段时间过后过期,请不要泄漏该地址" # type: ignore


def get_fastapi_driver() -> "Driver | None":
try:
from nonebot.drivers.fastapi import Driver

if (driver := get_driver()) and isinstance(driver, Driver):
return driver
return None

except ImportError:
return None


if (STATIC_PATH / "index.html").exists():
if driver := get_fastapi_driver():
init_fastapi(driver)
register_get_token_handler()
else:
logger.warning("your driver is not fastapi, webui feature will be disabled")
else:
logger.warning("Frontend file not found, please compile it or use docker or pypi version")
199 changes: 199 additions & 0 deletions src/plugins/nonebot_bison/admin_page/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,199 @@
import nonebot
from fastapi import status
from fastapi.routing import APIRouter
from fastapi.param_functions import Depends
from fastapi.exceptions import HTTPException
from nonebot_plugin_saa import TargetQQGroup
from nonebot_plugin_saa.auto_select_bot import get_bot
from fastapi.security.oauth2 import OAuth2PasswordBearer

from ..types import WeightConfig
from ..apis import check_sub_target
from .jwt import load_jwt, pack_jwt
from ..types import Target as T_Target
from ..utils.get_bot import get_groups
from ..platform import platform_manager
from .token_manager import token_manager
from ..config.db_config import SubscribeDupException
from ..config import NoSuchUserException, NoSuchTargetException, NoSuchSubscribeException, config
from .types import (
TokenResp,
GlobalConf,
StatusResp,
SubscribeResp,
PlatformConfig,
AddSubscribeReq,
SubscribeConfig,
SubscribeGroupDetail,
)

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(TargetQQGroup(group_id=group_id))
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": [
{
"id": info["group_id"],
"name": info["group_name"],
}
for info in 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(TargetQQGroup(group_id=group_id))
subs = [
SubscribeConfig(
platformName=sub.target.platform_name,
targetName=sub.target.target_name,
cats=sub.categories,
tags=sub.tags,
target=sub.target.target,
)
for sub in 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(
TargetQQGroup(group_id=groupNumber),
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(TargetQQGroup(group_id=groupNumber), 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(
TargetQQGroup(group_id=groupNumber),
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="")
Empty file.
22 changes: 22 additions & 0 deletions src/plugins/nonebot_bison/admin_page/jwt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import random
import string
import datetime

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) -> dict | None:
try:
return jwt.decode(token, _key, algorithms=["HS256"])
except Exception:
return None
Loading

0 comments on commit 8ec7b36

Please sign in to comment.