forked from forks/microblog.pub
Compare commits
2 commits
v2
...
quote-url-
Author | SHA1 | Date | |
---|---|---|---|
|
0f1fdd3944 | ||
|
254588f7c0 |
6 changed files with 124 additions and 5 deletions
|
@ -0,0 +1,34 @@
|
|||
"""Add support for quote URL
|
||||
|
||||
Revision ID: c3027d0e18dc
|
||||
Revises: 604d125ea2fb
|
||||
Create Date: 2022-09-21 07:08:24.568124+00:00
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'c3027d0e18dc'
|
||||
down_revision = '604d125ea2fb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('inbox', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('quoted_inbox_object_id', sa.Integer(), nullable=True))
|
||||
batch_op.create_foreign_key('fk_quoted_inbox_object_id', 'inbox', ['quoted_inbox_object_id'], ['id'])
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('inbox', schema=None) as batch_op:
|
||||
batch_op.drop_constraint('fk_quoted_inbox_object_id', type_='foreignkey')
|
||||
batch_op.drop_column('quoted_inbox_object_id')
|
||||
|
||||
# ### end Alembic commands ###
|
|
@ -2,9 +2,11 @@ import hashlib
|
|||
from datetime import datetime
|
||||
from functools import cached_property
|
||||
from typing import Any
|
||||
from typing import Optional
|
||||
|
||||
import pydantic
|
||||
from bs4 import BeautifulSoup # type: ignore
|
||||
from loguru import logger
|
||||
from markdown import markdown
|
||||
|
||||
from app import activitypub as ap
|
||||
|
@ -74,6 +76,10 @@ class Object:
|
|||
def tags(self) -> list[ap.RawObject]:
|
||||
return ap.as_list(self.ap_object.get("tag", []))
|
||||
|
||||
@property
|
||||
def quote_url(self) -> str | None:
|
||||
return self.ap_object.get("quoteUrl")
|
||||
|
||||
@cached_property
|
||||
def inlined_images(self) -> set[str]:
|
||||
image_urls: set[str] = set()
|
||||
|
@ -278,9 +284,15 @@ class Attachment(BaseModel):
|
|||
|
||||
|
||||
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._actor = actor
|
||||
self._quoted_object = quoted_object
|
||||
|
||||
if self._actor.ap_id != ap.get_actor_id(self._raw_object):
|
||||
raise ValueError(f"Invalid actor {self._actor.ap_id}")
|
||||
|
@ -290,6 +302,7 @@ class RemoteObject(Object):
|
|||
cls,
|
||||
raw_object: ap.RawObject,
|
||||
actor: Actor | None = None,
|
||||
fetch_quoted_url: bool = True,
|
||||
):
|
||||
# Pre-fetch the actor
|
||||
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)),
|
||||
)
|
||||
|
||||
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
|
||||
def og_meta(self) -> list[dict[str, Any]] | None:
|
||||
|
@ -319,3 +342,9 @@ class RemoteObject(Object):
|
|||
@property
|
||||
def actor(self) -> Actor:
|
||||
return self._actor
|
||||
|
||||
@property
|
||||
def quoted_object(self) -> Optional["RemoteObject"]:
|
||||
if self._quoted_object:
|
||||
return self._quoted_object
|
||||
return None
|
||||
|
|
30
app/boxes.py
30
app/boxes.py
|
@ -1576,8 +1576,11 @@ async def _process_note_object(
|
|||
from_actor: models.Actor,
|
||||
ro: RemoteObject,
|
||||
forwarded_by_actor: models.Actor | None = None,
|
||||
) -> None:
|
||||
if parent_activity.ap_type not in ["Create", "Read"]:
|
||||
process_quoted_url: bool = True,
|
||||
) -> 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}")
|
||||
|
||||
ap_published_at = now()
|
||||
|
@ -1620,6 +1623,7 @@ async def _process_note_object(
|
|||
),
|
||||
# We may already have some replies in DB
|
||||
replies_count=await _get_replies_count(db_session, ro.ap_id),
|
||||
quoted_inbox_object_id=None,
|
||||
)
|
||||
|
||||
db_session.add(inbox_object)
|
||||
|
@ -1700,6 +1704,28 @@ async def _process_note_object(
|
|||
)
|
||||
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(
|
||||
db_session: AsyncSession,
|
||||
|
|
|
@ -113,6 +113,18 @@ class InboxObject(Base, BaseObject):
|
|||
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)
|
||||
|
||||
# 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:
|
||||
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):
|
||||
__tablename__ = "outbox"
|
||||
|
@ -281,6 +299,10 @@ class OutboxObject(Base, BaseObject):
|
|||
def is_from_outbox(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def quoted_object(self) -> Optional["InboxObject"]:
|
||||
return None
|
||||
|
||||
|
||||
class Follower(Base):
|
||||
__tablename__ = "follower"
|
||||
|
|
|
@ -395,6 +395,13 @@
|
|||
{{ object.content | clean_html(object) | safe }}
|
||||
</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" %}
|
||||
{% 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 %}
|
||||
|
|
|
@ -66,7 +66,8 @@ async def external_urls(
|
|||
tags_hrefs = set()
|
||||
for tag in ro.tags:
|
||||
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["href"] != LOCAL_ACTOR.ap_id:
|
||||
mentioned_actor = await fetch_actor(db_session, tag["href"])
|
||||
|
|
Loading…
Reference in a new issue