mirror of
				https://git.sr.ht/~tsileo/microblog.pub
				synced 2025-06-05 21:59:23 +02:00 
			
		
		
		
	Add slug support for Article
This commit is contained in:
		| @@ -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" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user