mirror of
https://git.sr.ht/~tsileo/microblog.pub
synced 2025-06-05 21:59:23 +02:00
Compare commits
18 Commits
2.0.0-rc.7
...
2.0.0-rc.8
Author | SHA1 | Date | |
---|---|---|---|
d352dc104a | |||
0c5ce67d4e | |||
9db7bdf0fb | |||
793a939046 | |||
c3eb44add7 | |||
9b75020c91 | |||
36a1a6bd9c | |||
164cd9bd00 | |||
698a2bae11 | |||
4613997fe3 | |||
4c995957a6 | |||
5c98b8dbfb | |||
48d5914851 | |||
8f00e522d7 | |||
62c9327500 | |||
a339ff93b1 | |||
afd253a1b4 | |||
509e10e79b |
@ -154,6 +154,13 @@ if ALSO_KNOWN_AS:
|
||||
if MOVED_TO:
|
||||
ME["movedTo"] = MOVED_TO
|
||||
|
||||
if config.CONFIG.image_url:
|
||||
ME["image"] = {
|
||||
"mediaType": mimetypes.guess_type(config.CONFIG.image_url)[0],
|
||||
"type": "Image",
|
||||
"url": config.CONFIG.image_url,
|
||||
}
|
||||
|
||||
|
||||
class NotAnObjectError(Exception):
|
||||
def __init__(self, url: str, resp: httpx.Response | None = None) -> None:
|
||||
|
50
app/actor.py
50
app/actor.py
@ -82,11 +82,21 @@ class Actor:
|
||||
|
||||
@property
|
||||
def icon_url(self) -> str | None:
|
||||
return self.ap_actor.get("icon", {}).get("url")
|
||||
if icon := self.ap_actor.get("icon"):
|
||||
return icon.get("url")
|
||||
return None
|
||||
|
||||
@property
|
||||
def icon_media_type(self) -> str | None:
|
||||
return self.ap_actor.get("icon", {}).get("mediaType")
|
||||
if icon := self.ap_actor.get("icon"):
|
||||
return icon.get("mediaType")
|
||||
return None
|
||||
|
||||
@property
|
||||
def image_url(self) -> str | None:
|
||||
if image := self.ap_actor.get("image"):
|
||||
return image.get("url")
|
||||
return None
|
||||
|
||||
@property
|
||||
def public_key_as_pem(self) -> str:
|
||||
@ -214,24 +224,23 @@ async def fetch_actor(
|
||||
|
||||
if save_if_not_found:
|
||||
ap_actor = await ap.fetch(actor_id)
|
||||
# Some softwares uses URL when we expect ID
|
||||
if actor_id == ap_actor.get("url"):
|
||||
# Which mean we may already have it in DB
|
||||
existing_actor_by_url = (
|
||||
await db_session.scalars(
|
||||
select(models.Actor).where(
|
||||
models.Actor.ap_id == ap.get_id(ap_actor),
|
||||
)
|
||||
# Some softwares uses URL when we expect ID or uses a different casing
|
||||
# (like Birdsite LIVE) , which mean we may already have it in DB
|
||||
existing_actor_by_url = (
|
||||
await db_session.scalars(
|
||||
select(models.Actor).where(
|
||||
models.Actor.ap_id == ap.get_id(ap_actor),
|
||||
)
|
||||
).one_or_none()
|
||||
if existing_actor_by_url:
|
||||
# Update the actor as we had to fetch it anyway
|
||||
await update_actor_if_needed(
|
||||
db_session,
|
||||
existing_actor_by_url,
|
||||
RemoteActor(ap_actor),
|
||||
)
|
||||
return existing_actor_by_url
|
||||
)
|
||||
).one_or_none()
|
||||
if existing_actor_by_url:
|
||||
# Update the actor as we had to fetch it anyway
|
||||
await update_actor_if_needed(
|
||||
db_session,
|
||||
existing_actor_by_url,
|
||||
RemoteActor(ap_actor),
|
||||
)
|
||||
return existing_actor_by_url
|
||||
|
||||
return await save_actor(db_session, ap_actor)
|
||||
else:
|
||||
@ -381,6 +390,9 @@ def _actor_hash(actor: Actor) -> bytes:
|
||||
if actor.icon_url:
|
||||
h.update(actor.icon_url.encode())
|
||||
|
||||
if actor.image_url:
|
||||
h.update(actor.image_url.encode())
|
||||
|
||||
if actor.attachments:
|
||||
for a in actor.attachments:
|
||||
if a.get("type") != "PropertyValue":
|
||||
|
24
app/admin.py
24
app/admin.py
@ -850,6 +850,30 @@ async def admin_profile(
|
||||
)
|
||||
|
||||
|
||||
@router.post("/actions/force_delete")
|
||||
async def admin_actions_force_delete(
|
||||
request: Request,
|
||||
ap_object_id: str = Form(),
|
||||
redirect_url: str = Form(),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
) -> RedirectResponse:
|
||||
ap_object_to_delete = await get_inbox_object_by_ap_id(db_session, ap_object_id)
|
||||
if not ap_object_to_delete:
|
||||
raise ValueError(f"Cannot find {ap_object_id}")
|
||||
|
||||
logger.info(f"Deleting {ap_object_to_delete.ap_type}/{ap_object_to_delete.ap_id}")
|
||||
await boxes._revert_side_effect_for_deleted_object(
|
||||
db_session,
|
||||
None,
|
||||
ap_object_to_delete,
|
||||
None,
|
||||
)
|
||||
ap_object_to_delete.is_deleted = True
|
||||
await db_session.commit()
|
||||
return RedirectResponse(redirect_url, status_code=302)
|
||||
|
||||
|
||||
@router.post("/actions/follow")
|
||||
async def admin_actions_follow(
|
||||
request: Request,
|
||||
|
@ -280,6 +280,9 @@ class Attachment(BaseModel):
|
||||
proxied_url: str | None = None
|
||||
resized_url: str | None = None
|
||||
|
||||
width: int | None = None
|
||||
height: int | None = None
|
||||
|
||||
@property
|
||||
def mimetype(self) -> str:
|
||||
mimetype = self.media_type
|
||||
|
@ -1186,7 +1186,7 @@ async def _get_replies_count(
|
||||
|
||||
async def _revert_side_effect_for_deleted_object(
|
||||
db_session: AsyncSession,
|
||||
delete_activity: models.InboxObject,
|
||||
delete_activity: models.InboxObject | None,
|
||||
deleted_ap_object: models.InboxObject,
|
||||
forwarded_by_actor: models.Actor | None,
|
||||
) -> None:
|
||||
@ -1223,7 +1223,7 @@ async def _revert_side_effect_for_deleted_object(
|
||||
.where(
|
||||
models.OutboxObject.id == replied_object.id,
|
||||
)
|
||||
.values(replies_count=new_replies_count)
|
||||
.values(replies_count=new_replies_count - 1)
|
||||
)
|
||||
else:
|
||||
new_replies_count = await _get_replies_count(
|
||||
@ -1235,7 +1235,7 @@ async def _revert_side_effect_for_deleted_object(
|
||||
.where(
|
||||
models.InboxObject.id == replied_object.id,
|
||||
)
|
||||
.values(replies_count=new_replies_count)
|
||||
.values(replies_count=new_replies_count - 1)
|
||||
)
|
||||
|
||||
if deleted_ap_object.ap_type == "Like" and deleted_ap_object.activity_object_ap_id:
|
||||
@ -1282,7 +1282,8 @@ async def _revert_side_effect_for_deleted_object(
|
||||
# If it's a local replies, it was forwarded, so we also need to forward
|
||||
# the Delete activity if possible
|
||||
if (
|
||||
delete_activity.activity_object_ap_id == deleted_ap_object.ap_id
|
||||
delete_activity
|
||||
and delete_activity.activity_object_ap_id == deleted_ap_object.ap_id
|
||||
and delete_activity.has_ld_signature
|
||||
and is_delete_needs_to_be_forwarded
|
||||
):
|
||||
|
@ -92,6 +92,7 @@ class Config(pydantic.BaseModel):
|
||||
summary: str
|
||||
https: bool
|
||||
icon_url: str
|
||||
image_url: str | None = None
|
||||
secret: str
|
||||
debug: bool = False
|
||||
trusted_hosts: list[str] = ["127.0.0.1"]
|
||||
@ -109,6 +110,8 @@ class Config(pydantic.BaseModel):
|
||||
|
||||
inbox_retention_days: int = 15
|
||||
|
||||
custom_content_security_policy: str | None = None
|
||||
|
||||
# Config items to make tests easier
|
||||
sqlalchemy_database: str | None = None
|
||||
key_path: str | None = None
|
||||
@ -165,6 +168,7 @@ if CONFIG.privacy_replace:
|
||||
|
||||
BLOCKED_SERVERS = {blocked_server.hostname for blocked_server in CONFIG.blocked_servers}
|
||||
ALSO_KNOWN_AS = CONFIG.also_known_as
|
||||
CUSTOM_CONTENT_SECURITY_POLICY = CONFIG.custom_content_security_policy
|
||||
|
||||
INBOX_RETENTION_DAYS = CONFIG.inbox_retention_days
|
||||
CUSTOM_FOOTER = (
|
||||
|
@ -3,7 +3,6 @@ import traceback
|
||||
from datetime import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
import httpx
|
||||
from loguru import logger
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy import select
|
||||
@ -108,6 +107,7 @@ async def process_next_incoming_activity(
|
||||
|
||||
next_activity.tries = next_activity.tries + 1
|
||||
next_activity.last_try = now()
|
||||
await db_session.commit()
|
||||
|
||||
if next_activity.ap_object and next_activity.sent_by_ap_actor_id:
|
||||
try:
|
||||
@ -120,13 +120,16 @@ async def process_next_incoming_activity(
|
||||
),
|
||||
timeout=60,
|
||||
)
|
||||
except httpx.TimeoutException as exc:
|
||||
url = exc._request.url if exc._request else None
|
||||
logger.error(f"Failed, HTTP timeout when fetching {url}")
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
logger.error("Activity took too long to process")
|
||||
await db_session.rollback()
|
||||
await db_session.refresh(next_activity)
|
||||
next_activity.error = traceback.format_exc()
|
||||
_set_next_try(next_activity)
|
||||
except Exception:
|
||||
logger.exception("Failed")
|
||||
await db_session.rollback()
|
||||
await db_session.refresh(next_activity)
|
||||
next_activity.error = traceback.format_exc()
|
||||
_set_next_try(next_activity)
|
||||
else:
|
||||
|
54
app/main.py
54
app/main.py
@ -137,9 +137,15 @@ class CustomMiddleware:
|
||||
headers["x-frame-options"] = "DENY"
|
||||
headers["permissions-policy"] = "interest-cohort=()"
|
||||
headers["content-security-policy"] = (
|
||||
f"default-src 'self'; "
|
||||
f"style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}'; "
|
||||
f"frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
(
|
||||
f"default-src 'self'; "
|
||||
f"style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}'; "
|
||||
f"frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
)
|
||||
if not config.CUSTOM_CONTENT_SECURITY_POLICY
|
||||
else config.CUSTOM_CONTENT_SECURITY_POLICY.format(
|
||||
HIGHLIGHT_CSS_HASH=HIGHLIGHT_CSS_HASH
|
||||
)
|
||||
)
|
||||
if not DEBUG:
|
||||
headers["strict-transport-security"] = "max-age=63072000;"
|
||||
@ -248,6 +254,30 @@ class ActivityPubResponse(JSONResponse):
|
||||
media_type = "application/activity+json"
|
||||
|
||||
|
||||
async def redirect_to_remote_instance(
|
||||
request: Request,
|
||||
db_session: AsyncSession,
|
||||
url: str,
|
||||
) -> templates.TemplateResponse:
|
||||
"""
|
||||
Similar to RedirectResponse, but uses a 200 response with HTML.
|
||||
|
||||
Needed for remote redirects on form submission endpoints,
|
||||
since our CSP policy disallows remote form submission.
|
||||
https://github.com/w3c/webappsec-csp/issues/8#issuecomment-810108984
|
||||
"""
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"redirect_to_remote_instance.html",
|
||||
{
|
||||
"request": request,
|
||||
"url": url,
|
||||
},
|
||||
headers={"Refresh": "0;url=" + url},
|
||||
)
|
||||
|
||||
|
||||
@app.get(config.NavBarItems.NOTES_PATH)
|
||||
async def index(
|
||||
request: Request,
|
||||
@ -953,9 +983,10 @@ async def get_remote_follow(
|
||||
@app.post("/remote_follow")
|
||||
async def post_remote_follow(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
profile: str = Form(),
|
||||
) -> RedirectResponse:
|
||||
) -> templates.TemplateResponse:
|
||||
if not profile.startswith("@"):
|
||||
profile = f"@{profile}"
|
||||
|
||||
@ -964,9 +995,10 @@ async def post_remote_follow(
|
||||
# TODO(ts): error message to user
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
return RedirectResponse(
|
||||
return await redirect_to_remote_instance(
|
||||
request,
|
||||
db_session,
|
||||
remote_follow_template.format(uri=ID),
|
||||
status_code=302,
|
||||
)
|
||||
|
||||
|
||||
@ -994,10 +1026,11 @@ async def remote_interaction(
|
||||
@app.post("/remote_interaction")
|
||||
async def post_remote_interaction(
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
csrf_check: None = Depends(verify_csrf_token),
|
||||
profile: str = Form(),
|
||||
ap_id: str = Form(),
|
||||
) -> RedirectResponse:
|
||||
) -> templates.TemplateResponse:
|
||||
if not profile.startswith("@"):
|
||||
profile = f"@{profile}"
|
||||
|
||||
@ -1006,9 +1039,10 @@ async def post_remote_interaction(
|
||||
# TODO(ts): error message to user
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
return RedirectResponse(
|
||||
remote_follow_template.format(uri=ap_id),
|
||||
status_code=302,
|
||||
return await redirect_to_remote_instance(
|
||||
request,
|
||||
db_session,
|
||||
remote_follow_template.format(uri=ID),
|
||||
)
|
||||
|
||||
|
||||
|
@ -251,6 +251,8 @@ class OutboxObject(Base, BaseObject):
|
||||
"mediaType": attachment.upload.content_type,
|
||||
"name": attachment.alt or attachment.filename,
|
||||
"url": url,
|
||||
"width": attachment.upload.width,
|
||||
"height": attachment.upload.height,
|
||||
"proxiedUrl": url,
|
||||
"resizedUrl": BASE_URL
|
||||
+ (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import re
|
||||
import typing
|
||||
|
||||
from loguru import logger
|
||||
from mistletoe import Document # type: ignore
|
||||
from mistletoe.html_renderer import HTMLRenderer # type: ignore
|
||||
from mistletoe.span_token import SpanToken # type: ignore
|
||||
@ -78,13 +79,17 @@ class CustomRenderer(HTMLRenderer):
|
||||
|
||||
def render_mention(self, token: Mention) -> str:
|
||||
mention = token.target
|
||||
suffix = ""
|
||||
if mention.endswith("."):
|
||||
mention = mention[:-1]
|
||||
suffix = "."
|
||||
actor = self.mentioned_actors.get(mention)
|
||||
if not actor:
|
||||
return mention
|
||||
|
||||
self.tags.append(dict(type="Mention", href=actor.ap_id, name=mention))
|
||||
|
||||
link = f'<span class="h-card"><a href="{actor.url}" class="u-url mention">{actor.handle}</a></span>' # noqa: E501
|
||||
link = f'<span class="h-card"><a href="{actor.url}" class="u-url mention">{actor.handle}</a></span>{suffix}' # noqa: E501
|
||||
return link
|
||||
|
||||
def render_hashtag(self, token: Hashtag) -> str:
|
||||
@ -118,23 +123,30 @@ async def _prefetch_mentioned_actors(
|
||||
if mention in actors:
|
||||
continue
|
||||
|
||||
_, username, domain = mention.split("@")
|
||||
actor = (
|
||||
await db_session.execute(
|
||||
select(models.Actor).where(
|
||||
models.Actor.handle == mention,
|
||||
models.Actor.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not actor:
|
||||
actor_url = await webfinger.get_actor_url(mention)
|
||||
if not actor_url:
|
||||
# FIXME(ts): raise an error?
|
||||
continue
|
||||
actor = await fetch_actor(db_session, actor_url)
|
||||
# XXX: the regex catches stuff like `@toto@example.com.`
|
||||
if mention.endswith("."):
|
||||
mention = mention[:-1]
|
||||
|
||||
actors[mention] = actor
|
||||
try:
|
||||
_, username, domain = mention.split("@")
|
||||
actor = (
|
||||
await db_session.execute(
|
||||
select(models.Actor).where(
|
||||
models.Actor.handle == mention,
|
||||
models.Actor.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if not actor:
|
||||
actor_url = await webfinger.get_actor_url(mention)
|
||||
if not actor_url:
|
||||
# FIXME(ts): raise an error?
|
||||
continue
|
||||
actor = await fetch_actor(db_session, actor_url)
|
||||
|
||||
actors[mention] = actor
|
||||
except Exception:
|
||||
logger.exception(f"Failed to prefetch {mention}")
|
||||
|
||||
return actors
|
||||
|
||||
|
@ -85,6 +85,7 @@ async def render_template(
|
||||
template: str,
|
||||
template_args: dict[str, Any] | None = None,
|
||||
status_code: int = 200,
|
||||
headers: dict[str, str] | None = None,
|
||||
) -> TemplateResponse:
|
||||
if template_args is None:
|
||||
template_args = {}
|
||||
@ -129,6 +130,7 @@ async def render_template(
|
||||
**template_args,
|
||||
},
|
||||
status_code=status_code,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
|
||||
@ -424,3 +426,4 @@ _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
|
||||
_templates.env.globals["ICON_URL"] = config.CONFIG.icon_url
|
||||
|
@ -14,7 +14,7 @@
|
||||
<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="{{ ICON_URL }}" property="og:image" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
<meta content="{{ local_actor.handle }}" property="profile:username" />
|
||||
{% endif %}
|
||||
|
@ -26,11 +26,12 @@
|
||||
|
||||
{%- macro header_link(url, text) -%}
|
||||
{% set url_for = BASE_URL + request.app.router.url_path_for(url) %}
|
||||
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||
<a href="{{ url_for }}" {% if BASE_URL + 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>
|
||||
{% set url_for = BASE_URL + navbar_item[0] %}
|
||||
<a href="{{ navbar_item[0] }}" {% if BASE_URL + request.url.path == url_for %}class="active"{% endif %}>{{ navbar_item[1] }}</a>
|
||||
{% endmacro %}
|
||||
|
||||
<div class="public-top-menu">
|
||||
|
@ -13,7 +13,7 @@
|
||||
<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="{{ ICON_URL }}" property="og:image" />
|
||||
<meta content="summary" property="twitter:card" />
|
||||
<meta content="{{ local_actor.handle }}" property="profile:username" />
|
||||
{% endblock %}
|
||||
|
@ -19,7 +19,7 @@
|
||||
<div id="admin">
|
||||
{% macro admin_link(url, text) %}
|
||||
{% set url_for = BASE_URL + request.app.router.url_path_for(url) %}
|
||||
<a href="{{ url_for }}" {% if request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||
<a href="{{ url_for }}" {% if BASE_URL + request.url.path == url_for %}class="active"{% endif %}>{{ text }}</a>
|
||||
{% endmacro %}
|
||||
<div class="admin-menu">
|
||||
<nav class="flexbox">
|
||||
|
15
app/templates/redirect_to_remote_instance.html
Normal file
15
app/templates/redirect_to_remote_instance.html
Normal file
@ -0,0 +1,15 @@
|
||||
{%- import "utils.html" as utils with context -%}
|
||||
{% extends "layout.html" %}
|
||||
|
||||
{% block head %}
|
||||
<title>{{ local_actor.display_name }}'s microblog - Redirect</title>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
{% include "header.html" %}
|
||||
|
||||
<div class="box">
|
||||
<p>You are being redirected to your instance: <a href="{{ url }}">{{ url }}</a></p>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
@ -131,6 +131,17 @@
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_force_delete_button(ap_object_id, permalink_id=None) %}
|
||||
{% block admin_force_delete_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_force_delete") }}" class="object-delete-form" method="POST">
|
||||
{{ embed_csrf_token() }}
|
||||
{{ embed_redirect_url(permalink_id) }}
|
||||
<input type="hidden" name="ap_object_id" value="{{ ap_object_id }}">
|
||||
<input type="submit" value="local delete">
|
||||
</form>
|
||||
{% endblock %}
|
||||
{% endmacro %}
|
||||
|
||||
{% macro admin_announce_button(ap_object_id, permalink_id=None) %}
|
||||
{% block admin_announce_button scoped %}
|
||||
<form action="{{ request.url_for("admin_actions_announce") }}" method="POST">
|
||||
@ -384,16 +395,20 @@
|
||||
|
||||
{% for attachment in object.attachments %}
|
||||
{% if attachment.type != "PropertyValue" %}
|
||||
{% set orientation = "unknown" %}
|
||||
{% if attachment.width %}
|
||||
{% set orientation = "portrait" if attachment.width < attachment.height else "landscape" %}
|
||||
{% endif %}
|
||||
{% if object.sensitive and (attachment.type == "Image" or (attachment | has_media_type("image")) or attachment.type == "Video" or (attachment | has_media_type("video"))) %}
|
||||
<div class="attachment-wrapper">
|
||||
<label for="{{attachment.proxied_url}}" class="label-btn show-hide-sensitive-btn">show/hide sensitive content</label>
|
||||
<div>
|
||||
<div class="sensitive-attachment">
|
||||
<input class="sensitive-attachment-state" type="checkbox" id="{{attachment.proxied_url}}" aria-hidden="true">
|
||||
<div class="sensitive-attachment-box">
|
||||
<div class="sensitive-attachment-box attachment-orientation-{{orientation}}">
|
||||
<div></div>
|
||||
{% else %}
|
||||
<div class="attachment-item">
|
||||
<div class="attachment-item attachment-orientation-{{orientation}}">
|
||||
{% endif %}
|
||||
|
||||
{% if attachment.type == "Image" or (attachment | has_media_type("image")) %}
|
||||
@ -678,6 +693,11 @@
|
||||
{{ admin_expand_button(object) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% if object.is_from_inbox %}
|
||||
<li>
|
||||
{{ admin_force_delete_button(object.ap_id) }}
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
@ -1,12 +1,15 @@
|
||||
import asyncio
|
||||
import mimetypes
|
||||
import re
|
||||
import signal
|
||||
from concurrent.futures import TimeoutError
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import httpx
|
||||
from bs4 import BeautifulSoup # type: ignore
|
||||
from loguru import logger
|
||||
from pebble import concurrent # type: ignore
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app import activitypub as ap
|
||||
@ -29,7 +32,11 @@ class OpenGraphMeta(BaseModel):
|
||||
site_name: str
|
||||
|
||||
|
||||
@concurrent.process(timeout=5)
|
||||
def _scrap_og_meta(url: str, html: str) -> OpenGraphMeta | None:
|
||||
# Prevent SIGTERM to bubble up to the worker
|
||||
signal.signal(signal.SIGTERM, signal.SIG_IGN)
|
||||
|
||||
soup = BeautifulSoup(html, "html5lib")
|
||||
ogs = {
|
||||
og.attrs["property"]: og.attrs.get("content")
|
||||
@ -58,6 +65,10 @@ def _scrap_og_meta(url: str, html: str) -> OpenGraphMeta | None:
|
||||
return OpenGraphMeta.parse_obj(raw)
|
||||
|
||||
|
||||
def scrap_og_meta(url: str, html: str) -> OpenGraphMeta | None:
|
||||
return _scrap_og_meta(url, html).result()
|
||||
|
||||
|
||||
async def external_urls(
|
||||
db_session: AsyncSession,
|
||||
ro: ap_object.RemoteObject | OutboxObject | InboxObject,
|
||||
@ -126,7 +137,10 @@ async def _og_meta_from_url(url: str) -> OpenGraphMeta | None:
|
||||
return None
|
||||
|
||||
try:
|
||||
return _scrap_og_meta(url, resp.text)
|
||||
return scrap_og_meta(url, resp.text)
|
||||
except TimeoutError:
|
||||
logger.info(f"Timed out when scraping OG meta for {url}")
|
||||
return None
|
||||
except Exception:
|
||||
logger.info(f"Failed to scrap OG meta for {url}")
|
||||
return None
|
||||
|
@ -69,5 +69,5 @@ class Worker(Generic[T]):
|
||||
logger.info("stopping loop")
|
||||
|
||||
async def _shutdown(self, sig: signal.Signals) -> None:
|
||||
logger.info(f"Caught {signal=}")
|
||||
logger.info(f"Caught {sig=}")
|
||||
self._stop_event.set()
|
||||
|
@ -12,6 +12,7 @@ async def webfinger(
|
||||
resource: str,
|
||||
) -> dict[str, Any] | None: # noqa: C901
|
||||
"""Mastodon-like WebFinger resolution to retrieve the activity stream Actor URL."""
|
||||
resource = resource.strip()
|
||||
logger.info(f"performing webfinger resolution for {resource}")
|
||||
protos = ["https", "http"]
|
||||
if resource.startswith("http://"):
|
||||
|
@ -131,9 +131,19 @@ See `app/scss/main.scss` to see what variables can be overridden.
|
||||
|
||||
If you'd like to customize your instance's theme beyond CSS, you can modify the app's HTML by placing templates in `data/templates` which overwrite the defaults in `app/templates`.
|
||||
|
||||
#### Custom Content Security Policy (CSP)
|
||||
|
||||
You can override the default Content Security Policy by adding a line in `data/profile.toml`:
|
||||
|
||||
```toml
|
||||
custom_content_security_policy = "default-src 'self'; style-src 'self' 'sha256-{HIGHLIGHT_CSS_HASH}'; frame-ancestors 'none'; base-uri 'self'; form-action 'self';"
|
||||
```
|
||||
|
||||
This example will output the default CSP, note that `{HIGHLIGHT_CSS_HASH}` will be dynamically replaced by the correct value (the hash of the CSS needed for syntax highlighting).
|
||||
|
||||
#### Code highlighting theme
|
||||
|
||||
You can switch to one of the [styles supported by Pygments](https://pygments.org/styles/) by adding a line in `profile.toml`:
|
||||
You can switch to one of the [styles supported by Pygments](https://pygments.org/styles/) by adding a line in `data/profile.toml`:
|
||||
|
||||
```toml
|
||||
code_highlighting_theme = "solarized-dark"
|
||||
|
14
poetry.lock
generated
14
poetry.lock
generated
@ -689,6 +689,14 @@ category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pebble"
|
||||
version = "5.0.2"
|
||||
description = "Threading and multiprocessing eye-candy."
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "9.3.0"
|
||||
@ -1263,7 +1271,7 @@ dev = ["pytest (>=4.6.2)", "black (>=19.3b0)"]
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "89df524a545a19a20440d1872c93151bbf3f68d3b3d20cc50bc9049dd0e6d25f"
|
||||
content-hash = "13a1f5fc3f65c56e753062dca6ab74a50f7270d78a08ebf6297f7b4fa26b5eac"
|
||||
|
||||
[metadata.files]
|
||||
aiosqlite = [
|
||||
@ -1871,6 +1879,10 @@ pathspec = [
|
||||
{file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"},
|
||||
{file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"},
|
||||
]
|
||||
pebble = [
|
||||
{file = "Pebble-5.0.2-py3-none-any.whl", hash = "sha256:61b2dfd52b1a8c083b4e6cf3e0f1ff2e8a430a6283c53969a7057a1c91bed3cd"},
|
||||
{file = "Pebble-5.0.2.tar.gz", hash = "sha256:9c58c03eaf920c31287444c6fef39dc53baeac9de221ead104f5c9b48e8bd587"},
|
||||
]
|
||||
pillow = [
|
||||
{file = "Pillow-9.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2"},
|
||||
{file = "Pillow-9.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3"},
|
||||
|
@ -44,6 +44,7 @@ uvicorn = {extras = ["standard"], version = "^0.18.3"}
|
||||
Brotli = "^1.0.9"
|
||||
greenlet = "^1.1.3"
|
||||
mistletoe = "^0.9.0"
|
||||
Pebble = "^5.0.2"
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
black = "^22.3.0"
|
||||
|
Reference in New Issue
Block a user