-
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
88 changed files
with
6,188 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.