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 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
|
||||||
|
|
30
app/boxes.py
30
app/boxes.py
|
@ -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,
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 %}
|
||||||
|
|
|
@ -66,6 +66,7 @@ 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"):
|
||||||
|
if tag_href and tag_href not in filter(None, [ro.quote_url]):
|
||||||
tags_hrefs.add(tag_href)
|
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:
|
||||||
|
|
Loading…
Reference in a new issue