diff --git a/app/actor.py b/app/actor.py index 134f7e1..b906e2b 100644 --- a/app/actor.py +++ b/app/actor.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from typing import Union from urllib.parse import urlparse +from sqlalchemy import select from sqlalchemy.orm import Session 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": from app import models - existing_actor = ( - db.query(models.Actor).filter(models.Actor.ap_id == actor_id).one_or_none() - ) + existing_actor = db.execute( + select(models.Actor).where(models.Actor.ap_id == actor_id) + ).scalar_one_or_none() if existing_actor: return existing_actor @@ -183,27 +184,30 @@ def get_actors_metadata( ap_actor_ids = [actor.ap_id for actor in actors] followers = { follower.ap_actor_id: follower.inbox_object.ap_id - for follower in db.query(models.Follower) - .filter(models.Follower.ap_actor_id.in_(ap_actor_ids)) - .options(joinedload(models.Follower.inbox_object)) + for follower in db.scalars( + select(models.Follower) + .where(models.Follower.ap_actor_id.in_(ap_actor_ids)) + .options(joinedload(models.Follower.inbox_object)) + ) + .unique() .all() } following = { following.ap_actor_id - for following in db.query(models.Following.ap_actor_id) - .filter(models.Following.ap_actor_id.in_(ap_actor_ids)) - .all() + for following in db.execute( + select(models.Following.ap_actor_id).where( + models.Following.ap_actor_id.in_(ap_actor_ids) + ) + ) } sent_follow_requests = { follow_req.ap_object["object"]: follow_req.ap_id - for follow_req in db.query( - models.OutboxObject.ap_object, models.OutboxObject.ap_id + for follow_req in db.execute( + 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 = {} for actor in actors: diff --git a/app/admin.py b/app/admin.py index f1c0409..385c123 100644 --- a/app/admin.py +++ b/app/admin.py @@ -6,6 +6,8 @@ from fastapi import Request from fastapi import UploadFile from fastapi.exceptions import HTTPException from fastapi.responses import RedirectResponse +from sqlalchemy import func +from sqlalchemy import select from sqlalchemy.orm import Session from sqlalchemy.orm import joinedload @@ -141,16 +143,20 @@ def admin_bookmarks( db: Session = Depends(get_db), ) -> templates.TemplateResponse: stream = ( - db.query(models.InboxObject) - .filter( - models.InboxObject.ap_type.in_(["Note", "Article", "Video", "Announce"]), - models.InboxObject.is_hidden_from_stream.is_(False), - models.InboxObject.undone_by_inbox_object_id.is_(None), - models.InboxObject.is_bookmarked.is_(True), - ) - .order_by(models.InboxObject.ap_published_at.desc()) - .limit(20) - .all() + db.scalars( + select(models.InboxObject) + .where( + models.InboxObject.ap_type.in_( + ["Note", "Article", "Video", "Announce"] + ), + models.InboxObject.is_hidden_from_stream.is_(False), + models.InboxObject.undone_by_inbox_object_id.is_(None), + models.InboxObject.is_bookmarked.is_(True), + ) + .order_by(models.InboxObject.ap_published_at.desc()) + .limit(20) + ).all() + # TODO: joinedload + unique ) return templates.render_template( db, @@ -169,27 +175,28 @@ def admin_inbox( filter_by: str | None = None, cursor: str | None = None, ) -> templates.TemplateResponse: - q = db.query(models.InboxObject).filter( - models.InboxObject.ap_type.not_in(["Accept"]) - ) - + where = [models.InboxObject.ap_type.not_in(["Accept"])] if filter_by: - q = q.filter(models.InboxObject.ap_type == filter_by) + where.append(models.InboxObject.ap_type == filter_by) if cursor: - q = q.filter( + where.append( models.InboxObject.ap_published_at < pagination.decode_cursor(cursor) ) 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 = ( - q.options( - joinedload(models.InboxObject.relates_to_inbox_object), - joinedload(models.InboxObject.relates_to_outbox_object), + db.scalars( + q.options( + 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()) - .limit(20) + .unique() .all() ) @@ -227,27 +234,31 @@ def admin_outbox( filter_by: str | None = None, cursor: str | None = None, ) -> templates.TemplateResponse: - q = db.query(models.OutboxObject).filter( - models.OutboxObject.ap_type.not_in(["Accept"]) - ) + where = [models.OutboxObject.ap_type.not_in(["Accept"])] if filter_by: - q = q.filter(models.OutboxObject.ap_type == filter_by) + where.append(models.OutboxObject.ap_type == filter_by) if cursor: - q = q.filter( + where.append( models.OutboxObject.ap_published_at < pagination.decode_cursor(cursor) ) 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 = ( - q.options( - joinedload(models.OutboxObject.relates_to_inbox_object), - joinedload(models.OutboxObject.relates_to_outbox_object), - joinedload(models.OutboxObject.relates_to_actor), + db.scalars( + q.options( + joinedload(models.OutboxObject.relates_to_inbox_object), + 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()) - .limit(page_size) + .unique() .all() ) @@ -283,13 +294,16 @@ def get_notifications( request: Request, db: Session = Depends(get_db) ) -> templates.TemplateResponse: notifications = ( - db.query(models.Notification) - .options( - joinedload(models.Notification.actor), - joinedload(models.Notification.inbox_object), - joinedload(models.Notification.outbox_object), + db.scalars( + select(models.Notification) + .options( + joinedload(models.Notification.actor), + 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() ) actors_metadata = get_actors_metadata( @@ -337,21 +351,22 @@ def admin_profile( actor_id: str, db: Session = Depends(get_db), ) -> 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: raise HTTPException(status_code=404) actors_metadata = get_actors_metadata(db, [actor]) - inbox_objects = ( - db.query(models.InboxObject) - .filter( + inbox_objects = db.scalars( + select(models.InboxObject) + .where( models.InboxObject.actor_id == actor.id, models.InboxObject.ap_type.in_(["Note", "Article", "Video"]), ) .order_by(models.InboxObject.ap_published_at.desc()) - .all() - ) + ).all() return templates.render_template( db, diff --git a/app/boxes.py b/app/boxes.py index a89e24d..e8590d7 100644 --- a/app/boxes.py +++ b/app/boxes.py @@ -7,6 +7,10 @@ from urllib.parse import urlparse import httpx from dateutil.parser import isoparse 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.orm import Session from sqlalchemy.orm import joinedload @@ -189,9 +193,11 @@ def send_undo(db: Session, ap_object_id: str) -> None: outbox_object.id, ) # Also remove the follow from the following collection - db.query(models.Following).filter( - models.Following.ap_actor_id == followed_actor.ap_id - ).delete() + db.execute( + delete(models.Following).where( + models.Following.ap_actor_id == followed_actor.ap_id + ) + ) db.commit() elif outbox_object_to_undo.ap_type == "Like": 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 if in_reply_to_object.is_from_outbox: - db.query(models.OutboxObject).filter( - models.OutboxObject.ap_id == in_reply_to, - ).update({"replies_count": models.OutboxObject.replies_count + 1}) + db.execute( + update(models.OutboxObject) + .where( + models.OutboxObject.ap_id == in_reply_to, + ) + .values(replies_count=models.OutboxObject.replies_count + 1) + ) for (upload, filename) in uploads: attachments.append(upload_to_attachment(upload, filename)) @@ -339,9 +349,9 @@ def _compute_recipients(db: Session, ap_object: ap.RawObject) -> set[str]: continue # Is it a known actor? - known_actor = ( - db.query(models.Actor).filter(models.Actor.ap_id == r).one_or_none() - ) + known_actor = db.execute( + select(models.Actor).where(models.Actor.ap_id == r) + ).scalar_one_or_none() if known_actor: recipients.add(known_actor.shared_inbox_url or known_actor.inbox_url) 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: - return ( - db.query(models.InboxObject) - .filter(models.InboxObject.ap_id == ap_id) - .one_or_none() - ) + return db.execute( + select(models.InboxObject).where(models.InboxObject.ap_id == ap_id) + ).scalar_one_or_none() def get_outbox_object_by_ap_id(db: Session, ap_id: str) -> models.OutboxObject | None: - return ( - db.query(models.OutboxObject) - .filter(models.OutboxObject.ap_id == ap_id) - .one_or_none() - ) + return db.execute( + select(models.OutboxObject).where(models.OutboxObject.ap_id == ap_id) + ).scalar_one_or_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": logger.info(f"Undo follow from {from_actor.ap_id}") - db.query(models.Follower).filter( - models.Follower.inbox_object_id == ap_activity_to_undo.id - ).delete() + db.execute( + delete(models.Follower).where( + models.Follower.inbox_object_id == ap_activity_to_undo.id + ) + ) notif = models.Notification( notification_type=models.NotificationType.UNFOLLOW, actor_id=from_actor.id, @@ -536,9 +544,13 @@ def _handle_create_activity( return None if created_object.in_reply_to and created_object.in_reply_to.startswith(BASE_URL): - db.query(models.OutboxObject).filter( - models.OutboxObject.ap_id == created_object.in_reply_to, - ).update({"replies_count": models.OutboxObject.replies_count + 1}) + db.execute( + update(models.OutboxObject) + .where( + models.OutboxObject.ap_id == created_object.in_reply_to, + ) + .values(replies_count=models.OutboxObject.replies_count + 1) + ) for tag in tags: 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) if ( - db.query(models.InboxObject) - .filter(models.InboxObject.ap_id == ra.ap_id) - .count() + db.scalar( + select(func.count(models.InboxObject.id)).where( + models.InboxObject.ap_id == ra.ap_id + ) + ) > 0 ): 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: - return ( - db.query(models.OutboxObject) - .filter( + return db.scalar( + select(func.count(models.OutboxObject.id)).where( models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.is_deleted.is_(False), ) - .count() ) def fetch_actor_collection(db: Session, url: str) -> list[Actor]: if url.startswith(config.BASE_URL): if url == config.BASE_URL + "/followers": - q = db.query(models.Follower).options(joinedload(models.Follower.actor)) - return [follower.actor for follower in q.all()] + followers = ( + db.scalars( + select(models.Follower).options(joinedload(models.Follower.actor)) + ) + .unique() + .all() + ) + return [follower.actor for follower in followers] else: raise ValueError(f"internal collection for {url}) not supported") @@ -795,19 +813,19 @@ def get_replies_tree( # TODO: handle visibility tree_nodes: list[AnyboxObject] = [] tree_nodes.extend( - db.query(models.InboxObject) - .filter( - models.InboxObject.ap_context == requested_object.ap_context, - ) - .all() + db.scalars( + select(models.InboxObject).where( + models.InboxObject.ap_context == requested_object.ap_context, + ) + ).all() ) tree_nodes.extend( - db.query(models.OutboxObject) - .filter( - models.OutboxObject.ap_context == requested_object.ap_context, - models.OutboxObject.is_deleted.is_(False), - ) - .all() + db.scalars( + select(models.OutboxObject).where( + models.OutboxObject.ap_context == requested_object.ap_context, + models.OutboxObject.is_deleted.is_(False), + ) + ).all() ) nodes_by_in_reply_to = defaultdict(list) for node in tree_nodes: diff --git a/app/main.py b/app/main.py index c4cd531..8c260d9 100644 --- a/app/main.py +++ b/app/main.py @@ -22,6 +22,8 @@ from fastapi.staticfiles import StaticFiles from feedgen.feed import FeedGenerator # type: ignore from loguru import logger from PIL import Image +from sqlalchemy import func +from sqlalchemy import select from sqlalchemy.orm import Session from sqlalchemy.orm import joinedload from starlette.background import BackgroundTask @@ -147,24 +149,28 @@ def index( return ActivityPubResponse(LOCAL_ACTOR.ap_actor) page = page or 1 - q = db.query(models.OutboxObject).filter( + where = ( models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.is_deleted.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_offset = (page - 1) * page_size outbox_objects = ( - q.options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + db.scalars( + q.options( + 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()) - .offset(page_offset) - .limit(page_size) + .unique() .all() ) @@ -200,20 +206,22 @@ def _build_followx_collection( "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: - q = q.filter( + q = q.where( model_cls.created_at < pagination.decode_cursor(next_cursor) # type: ignore ) q = q.limit(20) - items = [followx for followx in q.all()] + items = [followx for followx in db.scalars(q).all()] next_cursor = None if ( items - and db.query(model_cls) - .filter(model_cls.created_at < items[-1].created_at) - .count() + and db.scalar( + select(func.count(model_cls.id)).where( + model_cls.created_at < items[-1].created_at + ) + ) > 0 ): 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 followers = ( - db.query(models.Follower) - .options(joinedload(models.Follower.actor)) - .order_by(models.Follower.created_at.desc()) - .limit(20) + db.scalars( + select(models.Follower) + .options(joinedload(models.Follower.actor)) + .order_by(models.Follower.created_at.desc()) + .limit(20) + ) + .unique() .all() ) @@ -303,13 +314,15 @@ def following( ) # We only show the most recent 20 follows on the public website - q = ( - db.query(models.Following) - .options(joinedload(models.Following.actor)) - .order_by(models.Following.created_at.desc()) - .limit(20) + following = ( + db.scalars( + select(models.Following) + .options(joinedload(models.Following.actor)) + .order_by(models.Following.created_at.desc()) + ) + .unique() + .all() ) - following = q.all() # TODO: support next_cursor/prev_cursor actors_metadata = {} @@ -336,16 +349,15 @@ def outbox( _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), ) -> ActivityPubResponse: # By design, we only show the last 20 public activities in the oubox - outbox_objects = ( - db.query(models.OutboxObject) - .filter( + outbox_objects = db.scalars( + select(models.OutboxObject) + .where( models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.is_deleted.is_(False), ) .order_by(models.OutboxObject.ap_published_at.desc()) .limit(20) - .all() - ) + ).all() return ActivityPubResponse( { "@context": ap.AS_EXTENDED_CTX, @@ -365,8 +377,8 @@ def featured( db: Session = Depends(get_db), _: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), ) -> ActivityPubResponse: - outbox_objects = ( - db.query(models.OutboxObject) + outbox_objects = db.scalars( + select(models.OutboxObject) .filter( models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.is_deleted.is_(False), @@ -374,8 +386,7 @@ def featured( ) .order_by(models.OutboxObject.ap_published_at.desc()) .limit(5) - .all() - ) + ).all() return ActivityPubResponse( { "@context": ap.AS_EXTENDED_CTX, @@ -421,17 +432,20 @@ def outbox_by_public_id( httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), ) -> ActivityPubResponse | templates.TemplateResponse: maybe_object = ( - db.query(models.OutboxObject) - .options( - joinedload(models.OutboxObject.outbox_object_attachments).options( - joinedload(models.OutboxObjectAttachment.upload) + db.execute( + select(models.OutboxObject) + .options( + 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( - models.OutboxObject.public_id == public_id, - models.OutboxObject.is_deleted.is_(False), - ) - .one_or_none() + .unique() + .scalar_one_or_none() ) if not maybe_object: raise HTTPException(status_code=404) @@ -444,25 +458,33 @@ def outbox_by_public_id( replies_tree = boxes.get_replies_tree(db, maybe_object) likes = ( - db.query(models.InboxObject) - .filter( - models.InboxObject.ap_type == "Like", - models.InboxObject.activity_object_ap_id == maybe_object.ap_id, + db.scalars( + select(models.InboxObject) + .where( + 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)) - .order_by(models.InboxObject.ap_published_at.desc()) - .limit(10) + .unique() + .all() ) shares = ( - db.query(models.InboxObject) - .filter( - models.InboxObject.ap_type == "Announce", - models.InboxObject.activity_object_ap_id == maybe_object.ap_id, + db.scalars( + select(models.InboxObject) + .filter( + 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)) - .order_by(models.InboxObject.ap_published_at.desc()) - .limit(10) + .unique() + .all() ) return templates.render_template( @@ -485,14 +507,12 @@ def outbox_activity_by_public_id( db: Session = Depends(get_db), httpsig_info: httpsig.HTTPSigInfo = Depends(httpsig.httpsig_checker), ) -> ActivityPubResponse: - maybe_object = ( - db.query(models.OutboxObject) - .filter( + maybe_object = db.execute( + select(models.OutboxObject).where( models.OutboxObject.public_id == public_id, models.OutboxObject.is_deleted.is_(False), ) - .one_or_none() - ) + ).scalar_one_or_none() if not maybe_object: raise HTTPException(status_code=404) @@ -765,13 +785,11 @@ def serve_attachment( filename: str, db: Session = Depends(get_db), ): - upload = ( - db.query(models.Upload) - .filter( + upload = db.execute( + select(models.Upload).where( models.Upload.content_hash == content_hash, ) - .one_or_none() - ) + ).scalar_one_or_none() if not upload: raise HTTPException(status_code=404) @@ -787,13 +805,11 @@ def serve_attachment_thumbnail( filename: str, db: Session = Depends(get_db), ): - upload = ( - db.query(models.Upload) - .filter( + upload = db.execute( + select(models.Upload).where( models.Upload.content_hash == content_hash, ) - .one_or_none() - ) + ).scalar_one_or_none() if not upload or not upload.has_thumbnail: raise HTTPException(status_code=404) @@ -812,17 +828,16 @@ Disallow: /admin""" def _get_outbox_for_feed(db: Session) -> list[models.OutboxObject]: - return ( - db.query(models.OutboxObject) - .filter( + return db.scalars( + select(models.OutboxObject) + .where( models.OutboxObject.visibility == ap.VisibilityEnum.PUBLIC, models.OutboxObject.is_deleted.is_(False), models.OutboxObject.ap_type.in_(["Note", "Article", "Video"]), ) .order_by(models.OutboxObject.ap_published_at.desc()) .limit(20) - .all() - ) + ).all() @app.get("/feed.json") diff --git a/app/outgoing_activities.py b/app/outgoing_activities.py index 826d2aa..7b965e2 100644 --- a/app/outgoing_activities.py +++ b/app/outgoing_activities.py @@ -6,6 +6,8 @@ from datetime import timedelta import httpx from loguru import logger +from sqlalchemy import func +from sqlalchemy import select from sqlalchemy.orm import Session from app import activitypub as ap @@ -67,22 +69,23 @@ def _set_next_try( def process_next_outgoing_activity(db: Session) -> bool: - q = ( - db.query(models.OutgoingActivity) - .filter( - models.OutgoingActivity.next_try <= now(), - models.OutgoingActivity.is_errored.is_(False), - models.OutgoingActivity.is_sent.is_(False), - ) - .order_by(models.OutgoingActivity.next_try) - ) - q_count = q.count() + where = [ + models.OutgoingActivity.next_try <= now(), + models.OutgoingActivity.is_errored.is_(False), + models.OutgoingActivity.is_sent.is_(False), + ] + q_count = db.scalar(select(func.count(models.OutgoingActivity.id)).where(*where)) logger.info(f"{q_count} outgoing activities ready to process") if not q_count: logger.info("No activities to process") 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.last_try = now() diff --git a/app/source.py b/app/source.py index 053a16e..f8b366b 100644 --- a/app/source.py +++ b/app/source.py @@ -1,6 +1,7 @@ import re from markdown import markdown +from sqlalchemy import select from sqlalchemy.orm import Session from app import models @@ -43,9 +44,9 @@ def _mentionify( mentioned_actors = [] for mention in re.findall(_MENTION_REGEX, content): _, username, domain = mention.split("@") - actor = ( - db.query(models.Actor).filter(models.Actor.handle == mention).one_or_none() - ) + actor = db.execute( + select(models.Actor).where(models.Actor.handle == mention) + ).scalar_one_or_none() if not actor: actor_url = webfinger.get_actor_url(mention) if not actor_url: diff --git a/app/templates.py b/app/templates.py index 6739ade..695ab74 100644 --- a/app/templates.py +++ b/app/templates.py @@ -13,6 +13,8 @@ from bs4 import BeautifulSoup # type: ignore from fastapi import Request from fastapi.templating import Jinja2Templates from loguru import logger +from sqlalchemy import func +from sqlalchemy import select from sqlalchemy.orm import Session from starlette.templating import _TemplateResponse as TemplateResponse @@ -94,14 +96,16 @@ def render_template( "csrf_token": generate_csrf_token() if is_admin else None, "highlight_css": HIGHLIGHT_CSS, "visibility_enum": ap.VisibilityEnum, - "notifications_count": db.query(models.Notification) - .filter(models.Notification.is_new.is_(True)) - .count() + "notifications_count": db.scalar( + select(func.count(models.Notification.id)).where( + models.Notification.is_new.is_(True) + ) + ) if is_admin else 0, "local_actor": LOCAL_ACTOR, - "followers_count": db.query(models.Follower).count(), - "following_count": db.query(models.Following).count(), + "followers_count": db.scalar(select(func.count(models.Follower.id))), + "following_count": db.scalar(select(func.count(models.Following.id))), **template_args, }, ) diff --git a/app/uploads.py b/app/uploads.py index 19a33c1..f1b241c 100644 --- a/app/uploads.py +++ b/app/uploads.py @@ -5,6 +5,7 @@ import blurhash # type: ignore from fastapi import UploadFile from loguru import logger from PIL import Image +from sqlalchemy import select from app import activitypub as ap from app import models @@ -27,11 +28,9 @@ def save_upload(db: Session, f: UploadFile) -> models.Upload: content_hash = h.hexdigest() f.file.seek(0) - existing_upload = ( - db.query(models.Upload) - .filter(models.Upload.content_hash == content_hash) - .one_or_none() - ) + existing_upload = db.execute( + select(models.Upload).where(models.Upload.content_hash == content_hash) + ).scalar_one_or_none() if existing_upload: logger.info(f"Upload with {content_hash=} already exists") return existing_upload