Switch to SQLAlchemy 2.0 query style

This commit is contained in:
Thomas Sileo 2022-06-29 08:56:39 +02:00
parent f4c70096e2
commit 18bd2cb664
8 changed files with 266 additions and 207 deletions

View file

@ -3,6 +3,7 @@ from dataclasses import dataclass
from typing import Union from typing import Union
from urllib.parse import urlparse from urllib.parse import urlparse
from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
@ -151,9 +152,9 @@ def save_actor(db: Session, ap_actor: ap.RawObject) -> "ActorModel":
def fetch_actor(db: Session, actor_id: str) -> "ActorModel": def fetch_actor(db: Session, actor_id: str) -> "ActorModel":
from app import models from app import models
existing_actor = ( existing_actor = db.execute(
db.query(models.Actor).filter(models.Actor.ap_id == actor_id).one_or_none() select(models.Actor).where(models.Actor.ap_id == actor_id)
) ).scalar_one_or_none()
if existing_actor: if existing_actor:
return existing_actor return existing_actor
@ -183,27 +184,30 @@ def get_actors_metadata(
ap_actor_ids = [actor.ap_id for actor in actors] ap_actor_ids = [actor.ap_id for actor in actors]
followers = { followers = {
follower.ap_actor_id: follower.inbox_object.ap_id follower.ap_actor_id: follower.inbox_object.ap_id
for follower in db.query(models.Follower) for follower in db.scalars(
.filter(models.Follower.ap_actor_id.in_(ap_actor_ids)) select(models.Follower)
.options(joinedload(models.Follower.inbox_object)) .where(models.Follower.ap_actor_id.in_(ap_actor_ids))
.options(joinedload(models.Follower.inbox_object))
)
.unique()
.all() .all()
} }
following = { following = {
following.ap_actor_id following.ap_actor_id
for following in db.query(models.Following.ap_actor_id) for following in db.execute(
.filter(models.Following.ap_actor_id.in_(ap_actor_ids)) select(models.Following.ap_actor_id).where(
.all() models.Following.ap_actor_id.in_(ap_actor_ids)
)
)
} }
sent_follow_requests = { sent_follow_requests = {
follow_req.ap_object["object"]: follow_req.ap_id follow_req.ap_object["object"]: follow_req.ap_id
for follow_req in db.query( for follow_req in db.execute(
models.OutboxObject.ap_object, models.OutboxObject.ap_id select(models.OutboxObject.ap_object, models.OutboxObject.ap_id).where(
models.OutboxObject.ap_type == "Follow",
models.OutboxObject.undone_by_outbox_object_id.is_(None),
)
) )
.filter(
models.OutboxObject.ap_type == "Follow",
models.OutboxObject.undone_by_outbox_object_id.is_(None),
)
.all()
} }
idx: ActorsMetadata = {} idx: ActorsMetadata = {}
for actor in actors: for actor in actors:

View file

@ -6,6 +6,8 @@ from fastapi import Request
from fastapi import UploadFile from fastapi import UploadFile
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
@ -141,16 +143,20 @@ def admin_bookmarks(
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> templates.TemplateResponse: ) -> templates.TemplateResponse:
stream = ( stream = (
db.query(models.InboxObject) db.scalars(
.filter( select(models.InboxObject)
models.InboxObject.ap_type.in_(["Note", "Article", "Video", "Announce"]), .where(
models.InboxObject.is_hidden_from_stream.is_(False), models.InboxObject.ap_type.in_(
models.InboxObject.undone_by_inbox_object_id.is_(None), ["Note", "Article", "Video", "Announce"]
models.InboxObject.is_bookmarked.is_(True), ),
) models.InboxObject.is_hidden_from_stream.is_(False),
.order_by(models.InboxObject.ap_published_at.desc()) models.InboxObject.undone_by_inbox_object_id.is_(None),
.limit(20) models.InboxObject.is_bookmarked.is_(True),
.all() )
.order_by(models.InboxObject.ap_published_at.desc())
.limit(20)
).all()
# TODO: joinedload + unique
) )
return templates.render_template( return templates.render_template(
db, db,
@ -169,27 +175,28 @@ def admin_inbox(
filter_by: str | None = None, filter_by: str | None = None,
cursor: str | None = None, cursor: str | None = None,
) -> templates.TemplateResponse: ) -> templates.TemplateResponse:
q = db.query(models.InboxObject).filter( where = [models.InboxObject.ap_type.not_in(["Accept"])]
models.InboxObject.ap_type.not_in(["Accept"])
)
if filter_by: if filter_by:
q = q.filter(models.InboxObject.ap_type == filter_by) where.append(models.InboxObject.ap_type == filter_by)
if cursor: if cursor:
q = q.filter( where.append(
models.InboxObject.ap_published_at < pagination.decode_cursor(cursor) models.InboxObject.ap_published_at < pagination.decode_cursor(cursor)
) )
page_size = 20 page_size = 20
remaining_count = q.count() remaining_count = db.scalar(select(func.count(models.InboxObject.id)).where(*where))
q = select(models.InboxObject).where(*where)
inbox = ( inbox = (
q.options( db.scalars(
joinedload(models.InboxObject.relates_to_inbox_object), q.options(
joinedload(models.InboxObject.relates_to_outbox_object), joinedload(models.InboxObject.relates_to_inbox_object),
joinedload(models.InboxObject.relates_to_outbox_object),
)
.order_by(models.InboxObject.ap_published_at.desc())
.limit(20)
) )
.order_by(models.InboxObject.ap_published_at.desc()) .unique()
.limit(20)
.all() .all()
) )
@ -227,27 +234,31 @@ def admin_outbox(
filter_by: str | None = None, filter_by: str | None = None,
cursor: str | None = None, cursor: str | None = None,
) -> templates.TemplateResponse: ) -> templates.TemplateResponse:
q = db.query(models.OutboxObject).filter( where = [models.OutboxObject.ap_type.not_in(["Accept"])]
models.OutboxObject.ap_type.not_in(["Accept"])
)
if filter_by: if filter_by:
q = q.filter(models.OutboxObject.ap_type == filter_by) where.append(models.OutboxObject.ap_type == filter_by)
if cursor: if cursor:
q = q.filter( where.append(
models.OutboxObject.ap_published_at < pagination.decode_cursor(cursor) models.OutboxObject.ap_published_at < pagination.decode_cursor(cursor)
) )
page_size = 20 page_size = 20
remaining_count = q.count() remaining_count = db.scalar(
select(func.count(models.OutboxObject.id)).where(*where)
)
q = select(models.OutboxObject).where(*where)
outbox = ( outbox = (
q.options( db.scalars(
joinedload(models.OutboxObject.relates_to_inbox_object), q.options(
joinedload(models.OutboxObject.relates_to_outbox_object), joinedload(models.OutboxObject.relates_to_inbox_object),
joinedload(models.OutboxObject.relates_to_actor), joinedload(models.OutboxObject.relates_to_outbox_object),
joinedload(models.OutboxObject.relates_to_actor),
)
.order_by(models.OutboxObject.ap_published_at.desc())
.limit(page_size)
) )
.order_by(models.OutboxObject.ap_published_at.desc()) .unique()
.limit(page_size)
.all() .all()
) )
@ -283,13 +294,16 @@ def get_notifications(
request: Request, db: Session = Depends(get_db) request: Request, db: Session = Depends(get_db)
) -> templates.TemplateResponse: ) -> templates.TemplateResponse:
notifications = ( notifications = (
db.query(models.Notification) db.scalars(
.options( select(models.Notification)
joinedload(models.Notification.actor), .options(
joinedload(models.Notification.inbox_object), joinedload(models.Notification.actor),
joinedload(models.Notification.outbox_object), joinedload(models.Notification.inbox_object),
joinedload(models.Notification.outbox_object),
)
.order_by(models.Notification.created_at.desc())
) )
.order_by(models.Notification.created_at.desc()) .unique()
.all() .all()
) )
actors_metadata = get_actors_metadata( actors_metadata = get_actors_metadata(
@ -337,21 +351,22 @@ def admin_profile(
actor_id: str, actor_id: str,
db: Session = Depends(get_db), db: Session = Depends(get_db),
) -> templates.TemplateResponse: ) -> templates.TemplateResponse:
actor = db.query(models.Actor).filter(models.Actor.ap_id == actor_id).one_or_none() actor = db.execute(
select(models.Actor).where(models.Actor.ap_id == actor_id)
).scalar_one_or_none()
if not actor: if not actor:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
actors_metadata = get_actors_metadata(db, [actor]) actors_metadata = get_actors_metadata(db, [actor])
inbox_objects = ( inbox_objects = db.scalars(
db.query(models.InboxObject) select(models.InboxObject)
.filter( .where(
models.InboxObject.actor_id == actor.id, models.InboxObject.actor_id == actor.id,
models.InboxObject.ap_type.in_(["Note", "Article", "Video"]), models.InboxObject.ap_type.in_(["Note", "Article", "Video"]),
) )
.order_by(models.InboxObject.ap_published_at.desc()) .order_by(models.InboxObject.ap_published_at.desc())
.all() ).all()
)
return templates.render_template( return templates.render_template(
db, db,

View file

@ -7,6 +7,10 @@ from urllib.parse import urlparse
import httpx import httpx
from dateutil.parser import isoparse from dateutil.parser import isoparse
from loguru import logger from loguru import logger
from sqlalchemy import delete
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy import update
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
@ -189,9 +193,11 @@ def send_undo(db: Session, ap_object_id: str) -> None:
outbox_object.id, outbox_object.id,
) )
# Also remove the follow from the following collection # Also remove the follow from the following collection
db.query(models.Following).filter( db.execute(
models.Following.ap_actor_id == followed_actor.ap_id delete(models.Following).where(
).delete() models.Following.ap_actor_id == followed_actor.ap_id
)
)
db.commit() db.commit()
elif outbox_object_to_undo.ap_type == "Like": elif outbox_object_to_undo.ap_type == "Like":
liked_object_ap_id = outbox_object_to_undo.activity_object_ap_id liked_object_ap_id = outbox_object_to_undo.activity_object_ap_id
@ -249,9 +255,13 @@ def send_create(
context = in_reply_to_object.ap_context context = in_reply_to_object.ap_context
if in_reply_to_object.is_from_outbox: if in_reply_to_object.is_from_outbox:
db.query(models.OutboxObject).filter( db.execute(
models.OutboxObject.ap_id == in_reply_to, update(models.OutboxObject)
).update({"replies_count": models.OutboxObject.replies_count + 1}) .where(
models.OutboxObject.ap_id == in_reply_to,
)
.values(replies_count=models.OutboxObject.replies_count + 1)
)
for (upload, filename) in uploads: for (upload, filename) in uploads:
attachments.append(upload_to_attachment(upload, filename)) attachments.append(upload_to_attachment(upload, filename))
@ -339,9 +349,9 @@ def _compute_recipients(db: Session, ap_object: ap.RawObject) -> set[str]:
continue continue
# Is it a known actor? # Is it a known actor?
known_actor = ( known_actor = db.execute(
db.query(models.Actor).filter(models.Actor.ap_id == r).one_or_none() select(models.Actor).where(models.Actor.ap_id == r)
) ).scalar_one_or_none()
if known_actor: if known_actor:
recipients.add(known_actor.shared_inbox_url or known_actor.inbox_url) recipients.add(known_actor.shared_inbox_url or known_actor.inbox_url)
continue continue
@ -361,19 +371,15 @@ def _compute_recipients(db: Session, ap_object: ap.RawObject) -> set[str]:
def get_inbox_object_by_ap_id(db: Session, ap_id: str) -> models.InboxObject | None: def get_inbox_object_by_ap_id(db: Session, ap_id: str) -> models.InboxObject | None:
return ( return db.execute(
db.query(models.InboxObject) select(models.InboxObject).where(models.InboxObject.ap_id == ap_id)
.filter(models.InboxObject.ap_id == ap_id) ).scalar_one_or_none()
.one_or_none()
)
def get_outbox_object_by_ap_id(db: Session, ap_id: str) -> models.OutboxObject | None: def get_outbox_object_by_ap_id(db: Session, ap_id: str) -> models.OutboxObject | None:
return ( return db.execute(
db.query(models.OutboxObject) select(models.OutboxObject).where(models.OutboxObject.ap_id == ap_id)
.filter(models.OutboxObject.ap_id == ap_id) ).scalar_one_or_none()
.one_or_none()
)
def get_anybox_object_by_ap_id(db: Session, ap_id: str) -> AnyboxObject | None: def get_anybox_object_by_ap_id(db: Session, ap_id: str) -> AnyboxObject | None:
@ -456,9 +462,11 @@ def _handle_undo_activity(
if ap_activity_to_undo.ap_type == "Follow": if ap_activity_to_undo.ap_type == "Follow":
logger.info(f"Undo follow from {from_actor.ap_id}") logger.info(f"Undo follow from {from_actor.ap_id}")
db.query(models.Follower).filter( db.execute(
models.Follower.inbox_object_id == ap_activity_to_undo.id delete(models.Follower).where(
).delete() models.Follower.inbox_object_id == ap_activity_to_undo.id
)
)
notif = models.Notification( notif = models.Notification(
notification_type=models.NotificationType.UNFOLLOW, notification_type=models.NotificationType.UNFOLLOW,
actor_id=from_actor.id, actor_id=from_actor.id,
@ -536,9 +544,13 @@ def _handle_create_activity(
return None return None
if created_object.in_reply_to and created_object.in_reply_to.startswith(BASE_URL): if created_object.in_reply_to and created_object.in_reply_to.startswith(BASE_URL):
db.query(models.OutboxObject).filter( db.execute(
models.OutboxObject.ap_id == created_object.in_reply_to, update(models.OutboxObject)
).update({"replies_count": models.OutboxObject.replies_count + 1}) .where(
models.OutboxObject.ap_id == created_object.in_reply_to,
)
.values(replies_count=models.OutboxObject.replies_count + 1)
)
for tag in tags: for tag in tags:
if tag.get("name") == LOCAL_ACTOR.handle or tag.get("href") == LOCAL_ACTOR.url: if tag.get("name") == LOCAL_ACTOR.handle or tag.get("href") == LOCAL_ACTOR.url:
@ -564,9 +576,11 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
ra = RemoteObject(ap.unwrap_activity(raw_object), actor=actor) ra = RemoteObject(ap.unwrap_activity(raw_object), actor=actor)
if ( if (
db.query(models.InboxObject) db.scalar(
.filter(models.InboxObject.ap_id == ra.ap_id) select(func.count(models.InboxObject.id)).where(
.count() models.InboxObject.ap_id == ra.ap_id
)
)
> 0 > 0
): ):
logger.info(f"Received duplicate {ra.ap_type} activity: {ra.ap_id}") logger.info(f"Received duplicate {ra.ap_type} activity: {ra.ap_id}")
@ -759,21 +773,25 @@ def save_to_inbox(db: Session, raw_object: ap.RawObject) -> None:
def public_outbox_objects_count(db: Session) -> int: def public_outbox_objects_count(db: Session) -> int:
return ( return db.scalar(
db.query(models.OutboxObject) select(func.count(models.OutboxObject.id)).where(
.filter(
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
) )
.count()
) )
def fetch_actor_collection(db: Session, url: str) -> list[Actor]: def fetch_actor_collection(db: Session, url: str) -> list[Actor]:
if url.startswith(config.BASE_URL): if url.startswith(config.BASE_URL):
if url == config.BASE_URL + "/followers": if url == config.BASE_URL + "/followers":
q = db.query(models.Follower).options(joinedload(models.Follower.actor)) followers = (
return [follower.actor for follower in q.all()] db.scalars(
select(models.Follower).options(joinedload(models.Follower.actor))
)
.unique()
.all()
)
return [follower.actor for follower in followers]
else: else:
raise ValueError(f"internal collection for {url}) not supported") raise ValueError(f"internal collection for {url}) not supported")
@ -795,19 +813,19 @@ def get_replies_tree(
# TODO: handle visibility # TODO: handle visibility
tree_nodes: list[AnyboxObject] = [] tree_nodes: list[AnyboxObject] = []
tree_nodes.extend( tree_nodes.extend(
db.query(models.InboxObject) db.scalars(
.filter( select(models.InboxObject).where(
models.InboxObject.ap_context == requested_object.ap_context, models.InboxObject.ap_context == requested_object.ap_context,
) )
.all() ).all()
) )
tree_nodes.extend( tree_nodes.extend(
db.query(models.OutboxObject) db.scalars(
.filter( select(models.OutboxObject).where(
models.OutboxObject.ap_context == requested_object.ap_context, models.OutboxObject.ap_context == requested_object.ap_context,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
) )
.all() ).all()
) )
nodes_by_in_reply_to = defaultdict(list) nodes_by_in_reply_to = defaultdict(list)
for node in tree_nodes: for node in tree_nodes:

View file

@ -22,6 +22,8 @@ from fastapi.staticfiles import StaticFiles
from feedgen.feed import FeedGenerator # type: ignore from feedgen.feed import FeedGenerator # type: ignore
from loguru import logger from loguru import logger
from PIL import Image from PIL import Image
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from sqlalchemy.orm import joinedload from sqlalchemy.orm import joinedload
from starlette.background import BackgroundTask from starlette.background import BackgroundTask
@ -147,24 +149,28 @@ def index(
return ActivityPubResponse(LOCAL_ACTOR.ap_actor) return ActivityPubResponse(LOCAL_ACTOR.ap_actor)
page = page or 1 page = page or 1
q = db.query(models.OutboxObject).filter( where = (
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
models.OutboxObject.is_hidden_from_homepage.is_(False), models.OutboxObject.is_hidden_from_homepage.is_(False),
) )
total_count = q.count() q = select(models.OutboxObject).where(*where)
total_count = db.scalar(select(func.count(models.OutboxObject.id)).where(*where))
page_size = 20 page_size = 20
page_offset = (page - 1) * page_size page_offset = (page - 1) * page_size
outbox_objects = ( outbox_objects = (
q.options( db.scalars(
joinedload(models.OutboxObject.outbox_object_attachments).options( q.options(
joinedload(models.OutboxObjectAttachment.upload) joinedload(models.OutboxObject.outbox_object_attachments).options(
joinedload(models.OutboxObjectAttachment.upload)
)
) )
.order_by(models.OutboxObject.ap_published_at.desc())
.offset(page_offset)
.limit(page_size)
) )
.order_by(models.OutboxObject.ap_published_at.desc()) .unique()
.offset(page_offset)
.limit(page_size)
.all() .all()
) )
@ -200,20 +206,22 @@ def _build_followx_collection(
"totalItems": total_items, "totalItems": total_items,
} }
q = db.query(model_cls).order_by(model_cls.created_at.desc()) # type: ignore q = select(model_cls).order_by(model_cls.created_at.desc()) # type: ignore
if next_cursor: if next_cursor:
q = q.filter( q = q.where(
model_cls.created_at < pagination.decode_cursor(next_cursor) # type: ignore model_cls.created_at < pagination.decode_cursor(next_cursor) # type: ignore
) )
q = q.limit(20) q = q.limit(20)
items = [followx for followx in q.all()] items = [followx for followx in db.scalars(q).all()]
next_cursor = None next_cursor = None
if ( if (
items items
and db.query(model_cls) and db.scalar(
.filter(model_cls.created_at < items[-1].created_at) select(func.count(model_cls.id)).where(
.count() model_cls.created_at < items[-1].created_at
)
)
> 0 > 0
): ):
next_cursor = pagination.encode_cursor(items[-1].created_at) next_cursor = pagination.encode_cursor(items[-1].created_at)
@ -257,10 +265,13 @@ def followers(
# We only show the most recent 20 followers on the public website # We only show the most recent 20 followers on the public website
followers = ( followers = (
db.query(models.Follower) db.scalars(
.options(joinedload(models.Follower.actor)) select(models.Follower)
.order_by(models.Follower.created_at.desc()) .options(joinedload(models.Follower.actor))
.limit(20) .order_by(models.Follower.created_at.desc())
.limit(20)
)
.unique()
.all() .all()
) )
@ -303,13 +314,15 @@ def following(
) )
# We only show the most recent 20 follows on the public website # We only show the most recent 20 follows on the public website
q = ( following = (
db.query(models.Following) db.scalars(
.options(joinedload(models.Following.actor)) select(models.Following)
.order_by(models.Following.created_at.desc()) .options(joinedload(models.Following.actor))
.limit(20) .order_by(models.Following.created_at.desc())
)
.unique()
.all()
) )
following = q.all()
# TODO: support next_cursor/prev_cursor # TODO: support next_cursor/prev_cursor
actors_metadata = {} actors_metadata = {}
@ -336,16 +349,15 @@ def outbox(
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse: ) -> ActivityPubResponse:
# By design, we only show the last 20 public activities in the oubox # By design, we only show the last 20 public activities in the oubox
outbox_objects = ( outbox_objects = db.scalars(
db.query(models.OutboxObject) select(models.OutboxObject)
.filter( .where(
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
) )
.order_by(models.OutboxObject.ap_published_at.desc()) .order_by(models.OutboxObject.ap_published_at.desc())
.limit(20) .limit(20)
.all() ).all()
)
return ActivityPubResponse( return ActivityPubResponse(
{ {
"@context": ap.AS_EXTENDED_CTX, "@context": ap.AS_EXTENDED_CTX,
@ -365,8 +377,8 @@ def featured(
db: Session = Depends(get_db), db: Session = Depends(get_db),
_: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse: ) -> ActivityPubResponse:
outbox_objects = ( outbox_objects = db.scalars(
db.query(models.OutboxObject) select(models.OutboxObject)
.filter( .filter(
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
@ -374,8 +386,7 @@ def featured(
) )
.order_by(models.OutboxObject.ap_published_at.desc()) .order_by(models.OutboxObject.ap_published_at.desc())
.limit(5) .limit(5)
.all() ).all()
)
return ActivityPubResponse( return ActivityPubResponse(
{ {
"@context": ap.AS_EXTENDED_CTX, "@context": ap.AS_EXTENDED_CTX,
@ -421,17 +432,20 @@ def outbox_by_public_id(
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse | templates.TemplateResponse: ) -> ActivityPubResponse | templates.TemplateResponse:
maybe_object = ( maybe_object = (
db.query(models.OutboxObject) db.execute(
.options( select(models.OutboxObject)
joinedload(models.OutboxObject.outbox_object_attachments).options( .options(
joinedload(models.OutboxObjectAttachment.upload) joinedload(models.OutboxObject.outbox_object_attachments).options(
joinedload(models.OutboxObjectAttachment.upload)
)
)
.where(
models.OutboxObject.public_id == public_id,
models.OutboxObject.is_deleted.is_(False),
) )
) )
.filter( .unique()
models.OutboxObject.public_id == public_id, .scalar_one_or_none()
models.OutboxObject.is_deleted.is_(False),
)
.one_or_none()
) )
if not maybe_object: if not maybe_object:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@ -444,25 +458,33 @@ def outbox_by_public_id(
replies_tree = boxes.get_replies_tree(db, maybe_object) replies_tree = boxes.get_replies_tree(db, maybe_object)
likes = ( likes = (
db.query(models.InboxObject) db.scalars(
.filter( select(models.InboxObject)
models.InboxObject.ap_type == "Like", .where(
models.InboxObject.activity_object_ap_id == maybe_object.ap_id, models.InboxObject.ap_type == "Like",
models.InboxObject.activity_object_ap_id == maybe_object.ap_id,
)
.options(joinedload(models.InboxObject.actor))
.order_by(models.InboxObject.ap_published_at.desc())
.limit(10)
) )
.options(joinedload(models.InboxObject.actor)) .unique()
.order_by(models.InboxObject.ap_published_at.desc()) .all()
.limit(10)
) )
shares = ( shares = (
db.query(models.InboxObject) db.scalars(
.filter( select(models.InboxObject)
models.InboxObject.ap_type == "Announce", .filter(
models.InboxObject.activity_object_ap_id == maybe_object.ap_id, models.InboxObject.ap_type == "Announce",
models.InboxObject.activity_object_ap_id == maybe_object.ap_id,
)
.options(joinedload(models.InboxObject.actor))
.order_by(models.InboxObject.ap_published_at.desc())
.limit(10)
) )
.options(joinedload(models.InboxObject.actor)) .unique()
.order_by(models.InboxObject.ap_published_at.desc()) .all()
.limit(10)
) )
return templates.render_template( return templates.render_template(
@ -485,14 +507,12 @@ def outbox_activity_by_public_id(
db: Session = Depends(get_db), db: Session = Depends(get_db),
httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker),
) -> ActivityPubResponse: ) -> ActivityPubResponse:
maybe_object = ( maybe_object = db.execute(
db.query(models.OutboxObject) select(models.OutboxObject).where(
.filter(
models.OutboxObject.public_id == public_id, models.OutboxObject.public_id == public_id,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
) )
.one_or_none() ).scalar_one_or_none()
)
if not maybe_object: if not maybe_object:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@ -765,13 +785,11 @@ def serve_attachment(
filename: str, filename: str,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
upload = ( upload = db.execute(
db.query(models.Upload) select(models.Upload).where(
.filter(
models.Upload.content_hash == content_hash, models.Upload.content_hash == content_hash,
) )
.one_or_none() ).scalar_one_or_none()
)
if not upload: if not upload:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@ -787,13 +805,11 @@ def serve_attachment_thumbnail(
filename: str, filename: str,
db: Session = Depends(get_db), db: Session = Depends(get_db),
): ):
upload = ( upload = db.execute(
db.query(models.Upload) select(models.Upload).where(
.filter(
models.Upload.content_hash == content_hash, models.Upload.content_hash == content_hash,
) )
.one_or_none() ).scalar_one_or_none()
)
if not upload or not upload.has_thumbnail: if not upload or not upload.has_thumbnail:
raise HTTPException(status_code=404) raise HTTPException(status_code=404)
@ -812,17 +828,16 @@ Disallow: /admin"""
def _get_outbox_for_feed(db: Session) -> list[models.OutboxObject]: def _get_outbox_for_feed(db: Session) -> list[models.OutboxObject]:
return ( return db.scalars(
db.query(models.OutboxObject) select(models.OutboxObject)
.filter( .where(
models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC,
models.OutboxObject.is_deleted.is_(False), models.OutboxObject.is_deleted.is_(False),
models.OutboxObject.ap_type.in_(["Note", "Article", "Video"]), models.OutboxObject.ap_type.in_(["Note", "Article", "Video"]),
) )
.order_by(models.OutboxObject.ap_published_at.desc()) .order_by(models.OutboxObject.ap_published_at.desc())
.limit(20) .limit(20)
.all() ).all()
)
@app.get("/feed.json") @app.get("/feed.json")

View file

@ -6,6 +6,8 @@ from datetime import timedelta
import httpx import httpx
from loguru import logger from loguru import logger
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app import activitypub as ap from app import activitypub as ap
@ -67,22 +69,23 @@ def _set_next_try(
def process_next_outgoing_activity(db: Session) -> bool: def process_next_outgoing_activity(db: Session) -> bool:
q = ( where = [
db.query(models.OutgoingActivity) models.OutgoingActivity.next_try <= now(),
.filter( models.OutgoingActivity.is_errored.is_(False),
models.OutgoingActivity.next_try <= now(), models.OutgoingActivity.is_sent.is_(False),
models.OutgoingActivity.is_errored.is_(False), ]
models.OutgoingActivity.is_sent.is_(False), q_count = db.scalar(select(func.count(models.OutgoingActivity.id)).where(*where))
)
.order_by(models.OutgoingActivity.next_try)
)
q_count = q.count()
logger.info(f"{q_count} outgoing activities ready to process") logger.info(f"{q_count} outgoing activities ready to process")
if not q_count: if not q_count:
logger.info("No activities to process") logger.info("No activities to process")
return False return False
next_activity = q.limit(1).one() next_activity = db.execute(
select(models.OutgoingActivity)
.where(*where)
.limit(1)
.order_by(models.OutgoingActivity.next_try)
).scalar_one()
next_activity.tries = next_activity.tries + 1 next_activity.tries = next_activity.tries + 1
next_activity.last_try = now() next_activity.last_try = now()

View file

@ -1,6 +1,7 @@
import re import re
from markdown import markdown from markdown import markdown
from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from app import models from app import models
@ -43,9 +44,9 @@ def _mentionify(
mentioned_actors = [] mentioned_actors = []
for mention in re.findall(_MENTION_REGEX, content): for mention in re.findall(_MENTION_REGEX, content):
_, username, domain = mention.split("@") _, username, domain = mention.split("@")
actor = ( actor = db.execute(
db.query(models.Actor).filter(models.Actor.handle == mention).one_or_none() select(models.Actor).where(models.Actor.handle == mention)
) ).scalar_one_or_none()
if not actor: if not actor:
actor_url = webfinger.get_actor_url(mention) actor_url = webfinger.get_actor_url(mention)
if not actor_url: if not actor_url:

View file

@ -13,6 +13,8 @@ from bs4 import BeautifulSoup # type: ignore
from fastapi import Request from fastapi import Request
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from loguru import logger from loguru import logger
from sqlalchemy import func
from sqlalchemy import select
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from starlette.templating import _TemplateResponse as TemplateResponse from starlette.templating import _TemplateResponse as TemplateResponse
@ -94,14 +96,16 @@ def render_template(
"csrf_token": generate_csrf_token() if is_admin else None, "csrf_token": generate_csrf_token() if is_admin else None,
"highlight_css": HIGHLIGHT_CSS, "highlight_css": HIGHLIGHT_CSS,
"visibility_enum": ap.VisibilityEnum, "visibility_enum": ap.VisibilityEnum,
"notifications_count": db.query(models.Notification) "notifications_count": db.scalar(
.filter(models.Notification.is_new.is_(True)) select(func.count(models.Notification.id)).where(
.count() models.Notification.is_new.is_(True)
)
)
if is_admin if is_admin
else 0, else 0,
"local_actor": LOCAL_ACTOR, "local_actor": LOCAL_ACTOR,
"followers_count": db.query(models.Follower).count(), "followers_count": db.scalar(select(func.count(models.Follower.id))),
"following_count": db.query(models.Following).count(), "following_count": db.scalar(select(func.count(models.Following.id))),
**template_args, **template_args,
}, },
) )

View file

@ -5,6 +5,7 @@ import blurhash # type: ignore
from fastapi import UploadFile from fastapi import UploadFile
from loguru import logger from loguru import logger
from PIL import Image from PIL import Image
from sqlalchemy import select
from app import activitypub as ap from app import activitypub as ap
from app import models from app import models
@ -27,11 +28,9 @@ def save_upload(db: Session, f: UploadFile) -> models.Upload:
content_hash = h.hexdigest() content_hash = h.hexdigest()
f.file.seek(0) f.file.seek(0)
existing_upload = ( existing_upload = db.execute(
db.query(models.Upload) select(models.Upload).where(models.Upload.content_hash == content_hash)
.filter(models.Upload.content_hash == content_hash) ).scalar_one_or_none()
.one_or_none()
)
if existing_upload: if existing_upload:
logger.info(f"Upload with {content_hash=} already exists") logger.info(f"Upload with {content_hash=} already exists")
return existing_upload return existing_upload