forked from forks/microblog.pub
Switch to SQLAlchemy 2.0 query style
This commit is contained in:
parent
f4c70096e2
commit
18bd2cb664
8 changed files with 266 additions and 207 deletions
36
app/actor.py
36
app/actor.py
|
@ -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:
|
||||||
|
|
107
app/admin.py
107
app/admin.py
|
@ -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,
|
||||||
|
|
108
app/boxes.py
108
app/boxes.py
|
@ -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:
|
||||||
|
|
167
app/main.py
167
app/main.py
|
@ -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")
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue