Boostrap support for quote URL

This commit is contained in:
Thomas Sileo 2022-09-21 19:18:44 +02:00
parent 4fcf585c23
commit 254588f7c0
5 changed files with 90 additions and 5 deletions

View file

@ -2,9 +2,11 @@ import hashlib
from datetime import datetime from datetime import datetime
from functools import cached_property from functools import cached_property
from typing import Any from typing import Any
from typing import Optional
import pydantic import pydantic
from bs4 import BeautifulSoup # type: ignore from bs4 import BeautifulSoup # type: ignore
from loguru import logger
from markdown import markdown from markdown import markdown
from app import activitypub as ap from app import activitypub as ap
@ -74,6 +76,10 @@ class Object:
def tags(self) -> list[ap.RawObject]: def tags(self) -> list[ap.RawObject]:
return ap.as_list(self.ap_object.get("tag", [])) return ap.as_list(self.ap_object.get("tag", []))
@property
def quote_url(self) -> str | None:
return self.ap_object.get("quoteUrl")
@cached_property @cached_property
def inlined_images(self) -> set[str]: def inlined_images(self) -> set[str]:
image_urls: set[str] = set() image_urls: set[str] = set()
@ -278,9 +284,15 @@ class Attachment(BaseModel):
class RemoteObject(Object): class RemoteObject(Object):
def __init__(self, raw_object: ap.RawObject, actor: Actor): def __init__(
self,
raw_object: ap.RawObject,
actor: Actor,
quoted_object: Object | None = None,
):
self._raw_object = raw_object self._raw_object = raw_object
self._actor = actor self._actor = actor
self._quoted_object = quoted_object
if self._actor.ap_id != ap.get_actor_id(self._raw_object): if self._actor.ap_id != ap.get_actor_id(self._raw_object):
raise ValueError(f"Invalid actor {self._actor.ap_id}") raise ValueError(f"Invalid actor {self._actor.ap_id}")
@ -290,6 +302,7 @@ class RemoteObject(Object):
cls, cls,
raw_object: ap.RawObject, raw_object: ap.RawObject,
actor: Actor | None = None, actor: Actor | None = None,
fetch_quoted_url: bool = True,
): ):
# Pre-fetch the actor # Pre-fetch the actor
actor_id = ap.get_actor_id(raw_object) actor_id = ap.get_actor_id(raw_object)
@ -306,7 +319,17 @@ class RemoteObject(Object):
ap_actor=await ap.fetch(ap.get_actor_id(raw_object)), ap_actor=await ap.fetch(ap.get_actor_id(raw_object)),
) )
return cls(raw_object, _actor) quoted_object: Object | None = None
if quote_url := raw_object.get("quoteUrl"):
try:
quoted_object = await RemoteObject.from_raw_object(
await ap.fetch(quote_url),
fetch_quoted_url=fetch_quoted_url,
)
except Exception:
logger.exception(f"Failed to fetch {quote_url=}")
return cls(raw_object, _actor, quoted_object=quoted_object)
@property @property
def og_meta(self) -> list[dict[str, Any]] | None: def og_meta(self) -> list[dict[str, Any]] | None:
@ -319,3 +342,9 @@ class RemoteObject(Object):
@property @property
def actor(self) -> Actor: def actor(self) -> Actor:
return self._actor return self._actor
@property
def quoted_object(self) -> Optional["RemoteObject"]:
if self._quoted_object:
return self._quoted_object
return None

View file

@ -1576,8 +1576,11 @@ async def _process_note_object(
from_actor: models.Actor, from_actor: models.Actor,
ro: RemoteObject, ro: RemoteObject,
forwarded_by_actor: models.Actor | None = None, forwarded_by_actor: models.Actor | None = None,
) -> None: process_quoted_url: bool = True,
if parent_activity.ap_type not in ["Create", "Read"]: ) -> models.InboxObject:
if process_quoted_url and parent_activity.quote_url == ro.ap_id:
logger.info(f"Processing quoted URL for {parent_activity.ap_id}")
elif parent_activity.ap_type not in ["Create", "Read"]:
raise ValueError(f"Unexpected parent activity {parent_activity.ap_id}") raise ValueError(f"Unexpected parent activity {parent_activity.ap_id}")
ap_published_at = now() ap_published_at = now()
@ -1620,6 +1623,7 @@ async def _process_note_object(
), ),
# We may already have some replies in DB # We may already have some replies in DB
replies_count=await _get_replies_count(db_session, ro.ap_id), replies_count=await _get_replies_count(db_session, ro.ap_id),
quoted_inbox_object_id=None,
) )
db_session.add(inbox_object) db_session.add(inbox_object)
@ -1700,6 +1704,28 @@ async def _process_note_object(
) )
db_session.add(notif) db_session.add(notif)
await db_session.flush()
if ro.quote_url and process_quoted_url:
try:
quoted_raw_object = await ap.fetch(ro.quote_url)
quoted_object_actor = await fetch_actor(
db_session, ap.get_actor_id(quoted_raw_object)
)
quoted_ro = RemoteObject(quoted_raw_object, quoted_object_actor)
quoted_inbox_object = await _process_note_object(
db_session,
inbox_object,
from_actor=quoted_object_actor,
ro=quoted_ro,
process_quoted_url=False,
)
inbox_object.quoted_inbox_object_id = quoted_inbox_object.id
except Exception:
logger.exception("Failed to process quoted object")
return inbox_object
async def _handle_vote_answer( async def _handle_vote_answer(
db_session: AsyncSession, db_session: AsyncSession,

View file

@ -113,6 +113,18 @@ class InboxObject(Base, BaseObject):
uselist=False, uselist=False,
) )
quoted_inbox_object_id = Column(
Integer,
ForeignKey("inbox.id", name="fk_quoted_inbox_object_id"),
nullable=True,
)
quoted_inbox_object: Mapped[Optional["InboxObject"]] = relationship(
"InboxObject",
foreign_keys=quoted_inbox_object_id,
remote_side=id,
uselist=False,
)
undone_by_inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=True) undone_by_inbox_object_id = Column(Integer, ForeignKey("inbox.id"), nullable=True)
# Link the oubox AP ID to allow undo without any extra query # Link the oubox AP ID to allow undo without any extra query
@ -147,6 +159,12 @@ class InboxObject(Base, BaseObject):
def is_from_inbox(self) -> bool: def is_from_inbox(self) -> bool:
return True return True
@property
def quoted_object(self) -> Optional["InboxObject"]:
if self.quoted_inbox_object_id:
return self.quoted_inbox_object
return None
class OutboxObject(Base, BaseObject): class OutboxObject(Base, BaseObject):
__tablename__ = "outbox" __tablename__ = "outbox"
@ -281,6 +299,10 @@ class OutboxObject(Base, BaseObject):
def is_from_outbox(self) -> bool: def is_from_outbox(self) -> bool:
return True return True
@property
def quoted_object(self) -> Optional["InboxObject"]:
return None
class Follower(Base): class Follower(Base):
__tablename__ = "follower" __tablename__ = "follower"

View file

@ -395,6 +395,13 @@
{{ object.content | clean_html(object) | safe }} {{ object.content | clean_html(object) | safe }}
</div> </div>
{% if object.quoted_object %}
<div class="ap-object-expanded ap-quoted-object">
{{ display_object(object.quoted_object) }}
</div>
{% endif %}
{% if object.ap_type == "Question" %} {% if object.ap_type == "Question" %}
{% set can_vote = is_admin and object.is_from_inbox and not object.is_poll_ended and not object.voted_for_answers %} {% set can_vote = is_admin and object.is_from_inbox and not object.is_poll_ended and not object.voted_for_answers %}
{% if can_vote %} {% if can_vote %}

View file

@ -66,7 +66,8 @@ async def external_urls(
tags_hrefs = set() tags_hrefs = set()
for tag in ro.tags: for tag in ro.tags:
if tag_href := tag.get("href"): if tag_href := tag.get("href"):
tags_hrefs.add(tag_href) if tag_href and tag_href not in filter(None, [ro.quote_url]):
tags_hrefs.add(tag_href)
if tag.get("type") == "Mention": if tag.get("type") == "Mention":
if tag["href"] != LOCAL_ACTOR.ap_id: if tag["href"] != LOCAL_ACTOR.ap_id:
mentioned_actor = await fetch_actor(db_session, tag["href"]) mentioned_actor = await fetch_actor(db_session, tag["href"])