From 32692a7dcdedeb17d7d7f2db0aa31e66b194dcc3 Mon Sep 17 00:00:00 2001 From: Thomas Sileo Date: Wed, 2 Nov 2022 08:51:21 +0100 Subject: [PATCH] First shot at supporting custom handler --- app/config.py | 26 +++++++ app/customization.py | 112 ++++++++++++++++++++++++++++++ app/main.py | 6 +- app/templates.py | 1 + app/templates/custom_page.html | 30 ++++++++ app/templates/header.html | 10 +++ app/utils/custom_index_handler.py | 32 +++++++++ 7 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 app/customization.py create mode 100644 app/templates/custom_page.html create mode 100644 app/utils/custom_index_handler.py diff --git a/app/config.py b/app/config.py index 993d5e0..dfd6ec9 100644 --- a/app/config.py +++ b/app/config.py @@ -14,6 +14,7 @@ from itsdangerous import URLSafeTimedSerializer from loguru import logger from mistletoe import markdown # type: ignore +from app.customization import _CUSTOM_ROUTES from app.utils.emoji import _load_emojis from app.utils.version import get_version_commit @@ -184,6 +185,31 @@ CODE_HIGHLIGHTING_THEME = CONFIG.code_highlighting_theme MOVED_TO = _get_moved_to() +_NavBarItem = tuple[str, str] + + +class NavBarItems: + EXTRA_NAVBAR_ITEMS: list[_NavBarItem] = [] + INDEX_NAVBAR_ITEM: _NavBarItem | None = None + NOTES_PATH = "/" + + +def load_custom_routes() -> None: + try: + from data import custom_routes # type: ignore # noqa: F401 + except ImportError: + pass + + for path, custom_handler in _CUSTOM_ROUTES.items(): + # If a handler wants to replace the root, move the index to /notes + if path == "/": + NavBarItems.NOTES_PATH = "/notes" + NavBarItems.INDEX_NAVBAR_ITEM = (path, custom_handler.title) + else: + if custom_handler.show_in_navbar: + NavBarItems.EXTRA_NAVBAR_ITEMS.append((path, custom_handler.title)) + + session_serializer = URLSafeTimedSerializer( CONFIG.secret, salt=f"{ID}.session", diff --git a/app/customization.py b/app/customization.py new file mode 100644 index 0000000..abb9062 --- /dev/null +++ b/app/customization.py @@ -0,0 +1,112 @@ +from pathlib import Path +from typing import Any +from typing import Callable + +from fastapi import APIRouter +from fastapi import Depends +from fastapi import Request +from starlette.responses import JSONResponse + +_DATA_DIR = Path().parent.resolve() / "data" +_Handler = Callable[..., Any] + + +class HTMLPage: + def __init__( + self, + title: str, + html_file: str, + show_in_navbar: bool, + ) -> None: + self.title = title + self.html_file = _DATA_DIR / html_file + self.show_in_navbar = show_in_navbar + + +class RawHandler: + def __init__( + self, + title: str, + handler: Any, + show_in_navbar: bool, + ) -> None: + self.title = title + self.handler = handler + self.show_in_navbar = show_in_navbar + + +_CUSTOM_ROUTES: dict[str, HTMLPage | RawHandler] = {} + + +def register_html_page( + path: str, + *, + title: str, + html_file: str, + show_in_navbar: bool = True, +) -> None: + if path in _CUSTOM_ROUTES: + raise ValueError(f"{path} is already registered") + + _CUSTOM_ROUTES[path] = HTMLPage(title, html_file, show_in_navbar) + + +def register_raw_handler( + path: str, + *, + title: str, + handler: _Handler, + show_in_navbar: bool = True, +) -> None: + if path in _CUSTOM_ROUTES: + raise ValueError(f"{path} is already registered") + + _CUSTOM_ROUTES[path] = RawHandler(title, handler, show_in_navbar) + + +class ActivityPubResponse(JSONResponse): + media_type = "application/activity+json" + + +def _custom_page_handler(path: str, html_page: HTMLPage) -> Any: + from app import templates + from app.actor import LOCAL_ACTOR + from app.config import is_activitypub_requested + from app.database import AsyncSession + from app.database import get_db_session + + async def _handler( + request: Request, + db_session: AsyncSession = Depends(get_db_session), + ) -> templates.TemplateResponse | ActivityPubResponse: + if path == "/" and is_activitypub_requested(request): + return ActivityPubResponse(LOCAL_ACTOR.ap_actor) + + return await templates.render_template( + db_session, + request, + "custom_page.html", + { + "page_content": html_page.html_file.read_text(), + "title": html_page.title, + }, + ) + + return _handler + + +def get_custom_router() -> APIRouter | None: + if not _CUSTOM_ROUTES: + return None + + router = APIRouter() + + for path, handler in _CUSTOM_ROUTES.items(): + if isinstance(handler, HTMLPage): + router.add_api_route( + path, _custom_page_handler(path, handler), methods=["GET"] + ) + else: + router.add_api_route(path, handler.handler) + + return router diff --git a/app/main.py b/app/main.py index 8cce9b3..7caf870 100644 --- a/app/main.py +++ b/app/main.py @@ -63,6 +63,7 @@ from app.config import USER_AGENT from app.config import USERNAME from app.config import is_activitypub_requested from app.config import verify_csrf_token +from app.customization import get_custom_router from app.database import AsyncSession from app.database import async_session from app.database import get_db_session @@ -192,6 +193,9 @@ app.include_router(admin.unauthenticated_router, prefix="/admin") app.include_router(indieauth.router) app.include_router(micropub.router) app.include_router(webmentions.router) +config.load_custom_routes() +if custom_router := get_custom_router(): + app.include_router(custom_router) # XXX: order matters, the proxy middleware needs to be last app.add_middleware(CustomMiddleware) @@ -243,7 +247,7 @@ class ActivityPubResponse(JSONResponse): media_type = "application/activity+json" -@app.get("/") +@app.get(config.NavBarItems.NOTES_PATH) async def index( request: Request, db_session: AsyncSession = Depends(get_db_session), diff --git a/app/templates.py b/app/templates.py index 07c4708..d75e506 100644 --- a/app/templates.py +++ b/app/templates.py @@ -429,3 +429,4 @@ _templates.env.globals["CSS_HASH"] = config.CSS_HASH _templates.env.globals["BASE_URL"] = config.BASE_URL _templates.env.globals["HIDES_FOLLOWERS"] = config.HIDES_FOLLOWERS _templates.env.globals["HIDES_FOLLOWING"] = config.HIDES_FOLLOWING +_templates.env.globals["NAVBAR_ITEMS"] = config.NavBarItems diff --git a/app/templates/custom_page.html b/app/templates/custom_page.html new file mode 100644 index 0000000..0797dd9 --- /dev/null +++ b/app/templates/custom_page.html @@ -0,0 +1,30 @@ +{%- import "utils.html" as utils with context -%} +{% extends "layout.html" %} + +{% block head %} +{{ title }} +{% if request.url.path == "/" %} + + + + + + + + + + + + + +{% endif %} +{% endblock %} + +{% block content %} +{% include "header.html" %} + +
+ {{ page_content | safe }} +
+ +{% endblock %} diff --git a/app/templates/header.html b/app/templates/header.html index d677408..99151a1 100644 --- a/app/templates/header.html +++ b/app/templates/header.html @@ -29,9 +29,16 @@ {{ text }} {% endmacro %} +{%- macro navbar_item_link(navbar_item) -%} +{{ navbar_item[1] }} +{% endmacro %} +
diff --git a/app/utils/custom_index_handler.py b/app/utils/custom_index_handler.py new file mode 100644 index 0000000..eb776e2 --- /dev/null +++ b/app/utils/custom_index_handler.py @@ -0,0 +1,32 @@ +from typing import Any +from typing import Awaitable +from typing import Callable + +from fastapi import Depends +from fastapi import Request +from fastapi.responses import JSONResponse + +from app.actor import LOCAL_ACTOR +from app.config import is_activitypub_requested +from app.database import AsyncSession +from app.database import get_db_session + +_Handler = Callable[[Request, AsyncSession], Awaitable[Any]] + + +def build_custom_index_handler(handler: _Handler) -> _Handler: + async def custom_index( + request: Request, + db_session: AsyncSession = Depends(get_db_session), + ) -> Any: + # Serve the AP actor if requested + if is_activitypub_requested(request): + return JSONResponse( + LOCAL_ACTOR.ap_actor, + media_type="application/activity+json", + ) + + # Defer to the custom handler + return await handler(request, db_session) + + return custom_index