First shot at supporting custom handler
This commit is contained in:
parent
817dd98c5c
commit
32692a7dcd
7 changed files with 216 additions and 1 deletions
|
@ -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",
|
||||
|
|
112
app/customization.py
Normal file
112
app/customization.py
Normal file
|
@ -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
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
|
|
30
app/templates/custom_page.html
Normal file
30
app/templates/custom_page.html
Normal file
|
@ -0,0 +1,30 @@
|
|||
{%- import "utils.html" as utils with context -%}
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ title }}</title>
|
||||
{% if request.url.path == "/" %}
|
||||
<link rel="indieauth-metadata" href="{{ url_for("well_known_authorization_server") }}">
|
||||
<link rel="authorization_endpoint" href="{{ url_for("indieauth_authorization_endpoint") }}">
|
||||
<link rel="token_endpoint" href="{{ url_for("indieauth_token_endpoint") }}">
|
||||
<link rel="micropub" href="{{ url_for("micropub_endpoint") }}">
|
||||
<link rel="alternate" href="{{ local_actor.url }}" title="ActivityPub profile" type="application/activity+json">
|
||||
<meta content="profile" property="og:type" />
|
||||
<meta content="{{ local_actor.url }}" property="og:url" />
|
||||
<meta content="{{ local_actor.display_name }}'s microblog" property="og:site_name" />
|
||||
<meta content="Homepage" property="og:title" />
|
||||
<meta content="{{ local_actor.summary | html2text | trim }}" property="og:description" />
|
||||
<meta content="{{ local_actor.url }}" property="og:image" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
<meta content="{{ local_actor.handle }}" property="profile:username" />
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "header.html" %}
|
||||
|
||||
<div class="box">
|
||||
{{ page_content | safe }}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
|
@ -29,9 +29,16 @@
|
|||
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||
{% endmacro %}
|
||||
|
||||
{%- macro navbar_item_link(navbar_item) -%}
|
||||
<a href="{{ navbar_item[0] }}" {% if request.url.path == navbar_item[0] %}class="active"{% endif %}>{{ navbar_item[1] }}</a>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="public-top-menu">
|
||||
<nav class="flexbox">
|
||||
<ul>
|
||||
{% if NAVBAR_ITEMS.INDEX_NAVBAR_ITEM %}
|
||||
<li>{{ navbar_item_link(NAVBAR_ITEMS.INDEX_NAVBAR_ITEM) }}</li>
|
||||
{% endif %}
|
||||
<li>{{ header_link("index", "Notes") }}</li>
|
||||
{% if articles_count %}
|
||||
<li>{{ header_link("articles", "Articles") }}</li>
|
||||
|
@ -43,6 +50,9 @@
|
|||
<li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
|
||||
{% endif %}
|
||||
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
|
||||
{% for navbar_item in NAVBAR_ITEMS.EXTRA_NAVBAR_ITEMS %}
|
||||
{{ navbar_item_link(navbar_item) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
|
32
app/utils/custom_index_handler.py
Normal file
32
app/utils/custom_index_handler.py
Normal file
|
@ -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
|
Loading…
Reference in a new issue