mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2024-12-22 13:14:28 +00:00
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 loguru import logger
|
||||||
from mistletoe import markdown # type: ignore
|
from mistletoe import markdown # type: ignore
|
||||||
|
|
||||||
|
from app.customization import _CUSTOM_ROUTES
|
||||||
from app.utils.emoji import _load_emojis
|
from app.utils.emoji import _load_emojis
|
||||||
from app.utils.version import get_version_commit
|
from app.utils.version import get_version_commit
|
||||||
|
|
||||||
|
@ -184,6 +185,31 @@ CODE_HIGHLIGHTING_THEME = CONFIG.code_highlighting_theme
|
||||||
MOVED_TO = _get_moved_to()
|
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(
|
session_serializer = URLSafeTimedSerializer(
|
||||||
CONFIG.secret,
|
CONFIG.secret,
|
||||||
salt=f"{ID}.session",
|
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 USERNAME
|
||||||
from app.config import is_activitypub_requested
|
from app.config import is_activitypub_requested
|
||||||
from app.config import verify_csrf_token
|
from app.config import verify_csrf_token
|
||||||
|
from app.customization import get_custom_router
|
||||||
from app.database import AsyncSession
|
from app.database import AsyncSession
|
||||||
from app.database import async_session
|
from app.database import async_session
|
||||||
from app.database import get_db_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(indieauth.router)
|
||||||
app.include_router(micropub.router)
|
app.include_router(micropub.router)
|
||||||
app.include_router(webmentions.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
|
# XXX: order matters, the proxy middleware needs to be last
|
||||||
app.add_middleware(CustomMiddleware)
|
app.add_middleware(CustomMiddleware)
|
||||||
|
@ -243,7 +247,7 @@ class ActivityPubResponse(JSONResponse):
|
||||||
media_type = "application/activity+json"
|
media_type = "application/activity+json"
|
||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get(config.NavBarItems.NOTES_PATH)
|
||||||
async def index(
|
async def index(
|
||||||
request: Request,
|
request: Request,
|
||||||
db_session: AsyncSession = Depends(get_db_session),
|
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["BASE_URL"] = config.BASE_URL
|
||||||
_templates.env.globals["HIDES_FOLLOWERS"] = config.HIDES_FOLLOWERS
|
_templates.env.globals["HIDES_FOLLOWERS"] = config.HIDES_FOLLOWERS
|
||||||
_templates.env.globals["HIDES_FOLLOWING"] = config.HIDES_FOLLOWING
|
_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>
|
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||||
{% endmacro %}
|
{% 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">
|
<div class="public-top-menu">
|
||||||
<nav class="flexbox">
|
<nav class="flexbox">
|
||||||
<ul>
|
<ul>
|
||||||
|
{% if NAVBAR_ITEMS.INDEX_NAVBAR_ITEM %}
|
||||||
|
<li>{{ navbar_item_link(NAVBAR_ITEMS.INDEX_NAVBAR_ITEM) }}</li>
|
||||||
|
{% endif %}
|
||||||
<li>{{ header_link("index", "Notes") }}</li>
|
<li>{{ header_link("index", "Notes") }}</li>
|
||||||
{% if articles_count %}
|
{% if articles_count %}
|
||||||
<li>{{ header_link("articles", "Articles") }}</li>
|
<li>{{ header_link("articles", "Articles") }}</li>
|
||||||
|
@ -43,6 +50,9 @@
|
||||||
<li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
|
<li>{{ header_link("following", "Following") }} <span class="counter">{{ following_count }}</span></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>{{ header_link("get_remote_follow", "Remote follow") }}</li>
|
<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>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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