Add slug support for Article
parent
fd5293a05c
commit
3d049da2e5
|
@ -0,0 +1,48 @@
|
|||
"""Add a slug field for outbox objects
|
||||
|
||||
Revision ID: b28c0551c236
|
||||
Revises: 604d125ea2fb
|
||||
Create Date: 2022-10-30 14:09:14.540461+00:00
|
||||
|
||||
"""
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm.session import Session
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = 'b28c0551c236'
|
||||
down_revision = '604d125ea2fb'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('outbox', schema=None) as batch_op:
|
||||
batch_op.add_column(sa.Column('slug', sa.String(), nullable=True))
|
||||
batch_op.create_index(batch_op.f('ix_outbox_slug'), ['slug'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
# Backfill the slug for existing articles
|
||||
from app.models import OutboxObject
|
||||
from app.utils.text import slugify
|
||||
sess = Session(op.get_bind())
|
||||
articles = sess.execute(select(OutboxObject).where(
|
||||
OutboxObject.ap_type == "Article")
|
||||
).scalars()
|
||||
for article in articles:
|
||||
title = article.ap_object["name"]
|
||||
article.slug = slugify(title)
|
||||
sess.commit()
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('outbox', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_outbox_slug'))
|
||||
batch_op.drop_column('slug')
|
||||
|
||||
# ### end Alembic commands ###
|
11
app/boxes.py
11
app/boxes.py
|
@ -41,6 +41,7 @@ from app.utils import webmentions
|
|||
from app.utils.datetime import as_utc
|
||||
from app.utils.datetime import now
|
||||
from app.utils.datetime import parse_isoformat
|
||||
from app.utils.text import slugify
|
||||
|
||||
AnyboxObject = models.InboxObject | models.OutboxObject
|
||||
|
||||
|
@ -63,6 +64,7 @@ async def save_outbox_object(
|
|||
source: str | None = None,
|
||||
is_transient: bool = False,
|
||||
conversation: str | None = None,
|
||||
slug: str | None = None,
|
||||
) -> models.OutboxObject:
|
||||
ro = await RemoteObject.from_raw_object(raw_object)
|
||||
|
||||
|
@ -82,6 +84,7 @@ async def save_outbox_object(
|
|||
source=source,
|
||||
is_transient=is_transient,
|
||||
conversation=conversation,
|
||||
slug=slug,
|
||||
)
|
||||
db_session.add(outbox_object)
|
||||
await db_session.flush()
|
||||
|
@ -614,6 +617,9 @@ async def send_create(
|
|||
else:
|
||||
raise ValueError(f"Unhandled visibility {visibility}")
|
||||
|
||||
slug = None
|
||||
url = outbox_object_id(note_id)
|
||||
|
||||
extra_obj_attrs = {}
|
||||
if ap_type == "Question":
|
||||
if not poll_answers or len(poll_answers) < 2:
|
||||
|
@ -643,6 +649,8 @@ async def send_create(
|
|||
if not name:
|
||||
raise ValueError("Article must have a name")
|
||||
|
||||
slug = slugify(name)
|
||||
url = f"{BASE_URL}/articles/{note_id[:7]}/{slug}"
|
||||
extra_obj_attrs = {"name": name}
|
||||
|
||||
obj = {
|
||||
|
@ -656,7 +664,7 @@ async def send_create(
|
|||
"published": published,
|
||||
"context": context,
|
||||
"conversation": context,
|
||||
"url": outbox_object_id(note_id),
|
||||
"url": url,
|
||||
"tag": dedup_tags(tags),
|
||||
"summary": content_warning,
|
||||
"inReplyTo": in_reply_to,
|
||||
|
@ -670,6 +678,7 @@ async def send_create(
|
|||
obj,
|
||||
source=source,
|
||||
conversation=conversation,
|
||||
slug=slug,
|
||||
)
|
||||
if not outbox_object.id:
|
||||
raise ValueError("Should never happen")
|
||||
|
|
162
app/main.py
162
app/main.py
|
@ -632,13 +632,75 @@ async def _check_outbox_object_acl(
|
|||
raise HTTPException(status_code=404)
|
||||
|
||||
|
||||
async def _fetch_likes(
|
||||
db_session: AsyncSession,
|
||||
outbox_object: models.OutboxObject,
|
||||
) -> list[models.InboxObject]:
|
||||
return (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.where(
|
||||
models.InboxObject.ap_type == "Like",
|
||||
models.InboxObject.activity_object_ap_id == outbox_object.ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
)
|
||||
.options(joinedload(models.InboxObject.actor))
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
async def _fetch_shares(
|
||||
db_session: AsyncSession,
|
||||
outbox_object: models.OutboxObject,
|
||||
) -> list[models.InboxObject]:
|
||||
return (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.filter(
|
||||
models.InboxObject.ap_type == "Announce",
|
||||
models.InboxObject.activity_object_ap_id == outbox_object.ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
)
|
||||
.options(joinedload(models.InboxObject.actor))
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
|
||||
async def _fetch_webmentions(
|
||||
db_session: AsyncSession,
|
||||
outbox_object: models.OutboxObject,
|
||||
) -> list[models.Webmention]:
|
||||
return (
|
||||
await db_session.scalars(
|
||||
select(models.Webmention)
|
||||
.filter(
|
||||
models.Webmention.outbox_object_id == outbox_object.id,
|
||||
models.Webmention.is_deleted.is_(False),
|
||||
)
|
||||
.limit(10)
|
||||
)
|
||||
).all()
|
||||
|
||||
|
||||
@app.get("/o/{public_id}")
|
||||
async def outbox_by_public_id(
|
||||
public_id: str,
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse:
|
||||
) -> ActivityPubResponse | templates.TemplateResponse | RedirectResponse:
|
||||
maybe_object = (
|
||||
(
|
||||
await db_session.execute(
|
||||
|
@ -665,59 +727,79 @@ async def outbox_by_public_id(
|
|||
if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(maybe_object.ap_object)
|
||||
|
||||
if maybe_object.ap_type == "Article":
|
||||
return RedirectResponse(
|
||||
f"/articles/{public_id[:7]}/{maybe_object.slug}",
|
||||
status_code=301,
|
||||
)
|
||||
|
||||
replies_tree = await boxes.get_replies_tree(
|
||||
db_session,
|
||||
maybe_object,
|
||||
is_current_user_admin=is_current_user_admin(request),
|
||||
)
|
||||
|
||||
likes = (
|
||||
likes = await _fetch_likes(db_session, maybe_object)
|
||||
shares = await _fetch_shares(db_session, maybe_object)
|
||||
webmentions = await _fetch_webmentions(db_session, maybe_object)
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
"object.html",
|
||||
{
|
||||
"replies_tree": replies_tree,
|
||||
"outbox_object": maybe_object,
|
||||
"likes": likes,
|
||||
"shares": shares,
|
||||
"webmentions": webmentions,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/articles/{short_id}/{slug}")
|
||||
async def article_by_slug(
|
||||
short_id: str,
|
||||
slug: str,
|
||||
request: Request,
|
||||
db_session: AsyncSession = Depends(get_db_session),
|
||||
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
|
||||
) -> ActivityPubResponse | templates.TemplateResponse | RedirectResponse:
|
||||
maybe_object = (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
await db_session.execute(
|
||||
select(models.OutboxObject)
|
||||
.options(
|
||||
joinedload(models.OutboxObject.outbox_object_attachments).options(
|
||||
joinedload(models.OutboxObjectAttachment.upload)
|
||||
)
|
||||
)
|
||||
.where(
|
||||
models.InboxObject.ap_type == "Like",
|
||||
models.InboxObject.activity_object_ap_id == maybe_object.ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
models.OutboxObject.public_id.like(f"{short_id}%"),
|
||||
models.OutboxObject.slug == slug,
|
||||
models.OutboxObject.is_deleted.is_(False),
|
||||
)
|
||||
.options(joinedload(models.InboxObject.actor))
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
.scalar_one_or_none()
|
||||
)
|
||||
if not maybe_object:
|
||||
raise HTTPException(status_code=404)
|
||||
|
||||
await _check_outbox_object_acl(request, db_session, maybe_object, httpsig_info)
|
||||
|
||||
if is_activitypub_requested(request):
|
||||
return ActivityPubResponse(maybe_object.ap_object)
|
||||
|
||||
replies_tree = await boxes.get_replies_tree(
|
||||
db_session,
|
||||
maybe_object,
|
||||
is_current_user_admin=is_current_user_admin(request),
|
||||
)
|
||||
|
||||
shares = (
|
||||
(
|
||||
await db_session.scalars(
|
||||
select(models.InboxObject)
|
||||
.filter(
|
||||
models.InboxObject.ap_type == "Announce",
|
||||
models.InboxObject.activity_object_ap_id == maybe_object.ap_id,
|
||||
models.InboxObject.is_deleted.is_(False),
|
||||
)
|
||||
.options(joinedload(models.InboxObject.actor))
|
||||
.order_by(models.InboxObject.ap_published_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
)
|
||||
.unique()
|
||||
.all()
|
||||
)
|
||||
|
||||
webmentions = (
|
||||
await db_session.scalars(
|
||||
select(models.Webmention)
|
||||
.filter(
|
||||
models.Webmention.outbox_object_id == maybe_object.id,
|
||||
models.Webmention.is_deleted.is_(False),
|
||||
)
|
||||
.limit(10)
|
||||
)
|
||||
).all()
|
||||
|
||||
likes = await _fetch_likes(db_session, maybe_object)
|
||||
shares = await _fetch_shares(db_session, maybe_object)
|
||||
webmentions = await _fetch_webmentions(db_session, maybe_object)
|
||||
return await templates.render_template(
|
||||
db_session,
|
||||
request,
|
||||
|
|
|
@ -158,6 +158,7 @@ class OutboxObject(Base, BaseObject):
|
|||
is_hidden_from_homepage = Column(Boolean, nullable=False, default=False)
|
||||
|
||||
public_id = Column(String, nullable=False, index=True)
|
||||
slug = Column(String, nullable=True, index=True)
|
||||
|
||||
ap_type = Column(String, nullable=False, index=True)
|
||||
ap_id: Mapped[str] = Column(String, nullable=False, unique=True, index=True)
|
||||
|
@ -281,6 +282,13 @@ class OutboxObject(Base, BaseObject):
|
|||
def is_from_outbox(self) -> bool:
|
||||
return True
|
||||
|
||||
@property
|
||||
def url(self) -> str | None:
|
||||
# XXX: rewrite old URL here for compat
|
||||
if self.ap_type == "Article" and self.slug and self.public_id:
|
||||
return f"{BASE_URL}/articles/{self.public_id[:7]}/{self.slug}"
|
||||
return super().url
|
||||
|
||||
|
||||
class Follower(Base):
|
||||
__tablename__ = "follower"
|
||||
|
|
Loading…
Reference in New Issue