mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2024-11-20 05:34:28 +00:00
191 lines
4.5 KiB
Python
191 lines
4.5 KiB
Python
|
import base64
|
||
|
from datetime import datetime
|
||
|
from datetime import timezone
|
||
|
from functools import lru_cache
|
||
|
from typing import Any
|
||
|
from urllib.parse import urlparse
|
||
|
|
||
|
import bleach
|
||
|
import timeago # type: ignore
|
||
|
from bs4 import BeautifulSoup # type: ignore
|
||
|
from fastapi import Request
|
||
|
from fastapi.templating import Jinja2Templates
|
||
|
from sqlalchemy.orm import Session
|
||
|
from starlette.templating import _TemplateResponse as TemplateResponse
|
||
|
|
||
|
from app import models
|
||
|
from app.actor import LOCAL_ACTOR
|
||
|
from app.ap_object import Attachment
|
||
|
from app.boxes import public_outbox_objects_count
|
||
|
from app.config import DEBUG
|
||
|
from app.config import DOMAIN
|
||
|
from app.config import VERSION
|
||
|
from app.config import generate_csrf_token
|
||
|
from app.config import session_serializer
|
||
|
from app.database import now
|
||
|
from app.highlight import HIGHLIGHT_CSS
|
||
|
from app.highlight import highlight
|
||
|
|
||
|
_templates = Jinja2Templates(directory="app/templates")
|
||
|
|
||
|
|
||
|
def _filter_domain(text: str) -> str:
|
||
|
hostname = urlparse(text).hostname
|
||
|
if not hostname:
|
||
|
raise ValueError(f"No hostname for {text}")
|
||
|
return hostname
|
||
|
|
||
|
|
||
|
def _media_proxy_url(url: str | None) -> str:
|
||
|
if not url:
|
||
|
return "/static/nopic.png"
|
||
|
|
||
|
if url.startswith(DOMAIN):
|
||
|
return url
|
||
|
|
||
|
encoded_url = base64.urlsafe_b64encode(url.encode()).decode()
|
||
|
return f"/proxy/media/{encoded_url}"
|
||
|
|
||
|
|
||
|
def is_current_user_admin(request: Request) -> bool:
|
||
|
is_admin = False
|
||
|
session_cookie = request.cookies.get("session")
|
||
|
if session_cookie:
|
||
|
try:
|
||
|
loaded_session = session_serializer.loads(
|
||
|
session_cookie,
|
||
|
max_age=3600 * 12,
|
||
|
)
|
||
|
except Exception:
|
||
|
pass
|
||
|
else:
|
||
|
is_admin = loaded_session.get("is_logged_in")
|
||
|
|
||
|
return is_admin
|
||
|
|
||
|
|
||
|
def render_template(
|
||
|
db: Session,
|
||
|
request: Request,
|
||
|
template: str,
|
||
|
template_args: dict[str, Any] = {},
|
||
|
) -> TemplateResponse:
|
||
|
is_admin = False
|
||
|
is_admin = is_current_user_admin(request)
|
||
|
|
||
|
return _templates.TemplateResponse(
|
||
|
template,
|
||
|
{
|
||
|
"request": request,
|
||
|
"debug": DEBUG,
|
||
|
"microblogpub_version": VERSION,
|
||
|
"is_admin": is_admin,
|
||
|
"csrf_token": generate_csrf_token() if is_admin else None,
|
||
|
"highlight_css": HIGHLIGHT_CSS,
|
||
|
"notifications_count": db.query(models.Notification)
|
||
|
.filter(models.Notification.is_new.is_(True))
|
||
|
.count()
|
||
|
if is_admin
|
||
|
else 0,
|
||
|
"local_actor": LOCAL_ACTOR,
|
||
|
"followers_count": db.query(models.Follower).count(),
|
||
|
"following_count": db.query(models.Following).count(),
|
||
|
"objects_count": public_outbox_objects_count(db),
|
||
|
**template_args,
|
||
|
},
|
||
|
)
|
||
|
|
||
|
|
||
|
# HTML/templates helper
|
||
|
ALLOWED_TAGS = [
|
||
|
"a",
|
||
|
"abbr",
|
||
|
"acronym",
|
||
|
"b",
|
||
|
"br",
|
||
|
"blockquote",
|
||
|
"code",
|
||
|
"pre",
|
||
|
"em",
|
||
|
"i",
|
||
|
"li",
|
||
|
"ol",
|
||
|
"strong",
|
||
|
"sup",
|
||
|
"sub",
|
||
|
"del",
|
||
|
"ul",
|
||
|
"span",
|
||
|
"div",
|
||
|
"p",
|
||
|
"h1",
|
||
|
"h2",
|
||
|
"h3",
|
||
|
"h4",
|
||
|
"h5",
|
||
|
"h6",
|
||
|
"table",
|
||
|
"th",
|
||
|
"tr",
|
||
|
"td",
|
||
|
"thead",
|
||
|
"tbody",
|
||
|
"tfoot",
|
||
|
"colgroup",
|
||
|
"caption",
|
||
|
"img",
|
||
|
]
|
||
|
|
||
|
ALLOWED_ATTRIBUTES = {
|
||
|
"a": ["href", "title"],
|
||
|
"abbr": ["title"],
|
||
|
"acronym": ["title"],
|
||
|
"img": ["src", "alt", "title"],
|
||
|
}
|
||
|
|
||
|
|
||
|
@lru_cache(maxsize=256)
|
||
|
def _update_inline_imgs(content):
|
||
|
soup = BeautifulSoup(content, "html5lib")
|
||
|
imgs = soup.find_all("img")
|
||
|
if not imgs:
|
||
|
return content
|
||
|
|
||
|
for img in imgs:
|
||
|
if not img.attrs.get("src"):
|
||
|
continue
|
||
|
|
||
|
img.attrs["src"] = _media_proxy_url(img.attrs["src"])
|
||
|
|
||
|
return soup.find("body").decode_contents()
|
||
|
|
||
|
|
||
|
def _clean_html(html: str) -> str:
|
||
|
try:
|
||
|
return bleach.clean(
|
||
|
_update_inline_imgs(highlight(html)),
|
||
|
tags=ALLOWED_TAGS,
|
||
|
attributes=ALLOWED_ATTRIBUTES,
|
||
|
strip=True,
|
||
|
)
|
||
|
except Exception:
|
||
|
raise
|
||
|
|
||
|
|
||
|
def _timeago(original_dt: datetime) -> str:
|
||
|
dt = original_dt
|
||
|
if dt.tzinfo:
|
||
|
dt = dt.astimezone(timezone.utc).replace(tzinfo=None)
|
||
|
return timeago.format(dt, now().replace(tzinfo=None))
|
||
|
|
||
|
|
||
|
def _has_media_type(attachment: Attachment, media_type_prefix: str) -> bool:
|
||
|
return attachment.media_type.startswith(media_type_prefix)
|
||
|
|
||
|
|
||
|
_templates.env.filters["domain"] = _filter_domain
|
||
|
_templates.env.filters["media_proxy_url"] = _media_proxy_url
|
||
|
_templates.env.filters["clean_html"] = _clean_html
|
||
|
_templates.env.filters["timeago"] = _timeago
|
||
|
_templates.env.filters["has_media_type"] = _has_media_type
|