diff --git a/app/admin.py b/app/admin.py index 11ca702..ece448e 100644 --- a/app/admin.py +++ b/app/admin.py @@ -1,3 +1,5 @@ +from datetime import datetime + import httpx from fastapi import APIRouter from fastapi import Cookie @@ -8,7 +10,9 @@ from fastapi import UploadFile from fastapi.exceptions import HTTPException from fastapi.responses import RedirectResponse from loguru import logger +from sqlalchemy import and_ from sqlalchemy import func +from sqlalchemy import or_ from sqlalchemy import select from sqlalchemy.orm import joinedload @@ -383,6 +387,173 @@ async def admin_inbox( ) +@router.get("/direct_messages") +async def admin_direct_messages( + request: Request, + db_session: AsyncSession = Depends(get_db_session), + cursor: str | None = None, +) -> templates.TemplateResponse: + inbox_convos = ( + ( + await db_session.execute( + select( + models.InboxObject.ap_context, + models.InboxObject.actor_id, + func.count(1).label("count"), + func.max(models.InboxObject.ap_published_at).label( + "most_recent_date" + ), + ) + .where( + models.InboxObject.visibility == ap.VisibilityEnum.DIRECT, + models.InboxObject.ap_context.is_not(None), + ) + .group_by(models.InboxObject.ap_context, models.InboxObject.actor_id) + ) + ) + .unique() + .all() + ) + outbox_convos = ( + ( + await db_session.execute( + select( + models.OutboxObject.ap_context, + func.count(1).label("count"), + func.max(models.OutboxObject.ap_published_at).label( + "most_recent_date" + ), + ) + .where( + models.OutboxObject.visibility == ap.VisibilityEnum.DIRECT, + models.OutboxObject.ap_context.is_not(None), + ) + .group_by(models.OutboxObject.ap_context) + ) + ) + .unique() + .all() + ) + + convos = {} + for inbox_convo in inbox_convos: + if inbox_convo.ap_context not in convos: + convos[inbox_convo.ap_context] = { + "actor_ids": {inbox_convo.actor_id}, + "count": inbox_convo.count, + "most_recent_from_inbox": inbox_convo.most_recent_date, + "most_recent_from_outbox": datetime.min, + } + else: + convos[inbox_convo.ap_context]["actor_ids"].add(inbox_convo.actor_id) + convos[inbox_convo.ap_context]["count"] += inbox_convo.count + convos[inbox_convo.ap_context]["most_recent_from_inbox"] = max( + inbox_convo.most_recent_date, + convos[inbox_convo.ap_context]["most_recent_from_inbox"], + ) + + for outbox_convo in outbox_convos: + if outbox_convo.ap_context not in convos: + convos[outbox_convo.ap_context] = { + "actor_ids": set(), + "count": outbox_convo.count, + "most_recent_from_inbox": datetime.min, + "most_recent_from_outbox": outbox_convo.most_recent_date, + } + else: + convos[outbox_convo.ap_context]["count"] += outbox_convo.count + convos[outbox_convo.ap_context]["most_recent_from_outbox"] = max( + outbox_convo.most_recent_date, + convos[outbox_convo.ap_context]["most_recent_from_outbox"], + ) + + convos_with_last_from_inbox = [] + convos_with_last_from_outbox = [] + for context, convo in convos.items(): + if convo["most_recent_from_inbox"] > convo["most_recent_from_outbox"]: + convos_with_last_from_inbox.append( + and_( + models.InboxObject.ap_context == context, + models.InboxObject.ap_published_at + == convo["most_recent_from_inbox"], + ) + ) + else: + convos_with_last_from_outbox.append( + and_( + models.OutboxObject.ap_context == context, + models.OutboxObject.ap_published_at + == convo["most_recent_from_outbox"], + ) + ) + last_from_inbox = ( + ( + await db_session.scalars( + select(models.InboxObject) + .where(or_(*convos_with_last_from_inbox)) + .options( + joinedload(models.InboxObject.actor), + ) + ) + ) + .unique() + .all() + ) + last_from_outbox = ( + ( + await db_session.scalars( + select(models.OutboxObject) + .where(or_(*convos_with_last_from_outbox)) + .options( + joinedload(models.OutboxObject.outbox_object_attachments).options( + joinedload(models.OutboxObjectAttachment.upload) + ), + ) + ) + ) + .unique() + .all() + ) + threads = [] + for anybox_object in sorted( + last_from_inbox + last_from_outbox, + key=lambda x: x.ap_published_at, + reverse=True, + ): + convo = convos[anybox_object.ap_context] + actors = list( + ( + await db_session.execute( + select(models.Actor).where(models.Actor.id.in_(convo["actor_ids"])) + ) + ).scalars() + ) + # If this message from outbox starts a thread with no replies, look + # at the mentions + if not actors and anybox_object.is_from_outbox: + actors = ( # type: ignore + await db_session.execute( + select(models.Actor).where( + models.Actor.ap_id.in_( + mention["href"] + for mention in anybox_object.tags + if mention["type"] == "Mention" + ) + ) + ) + ).scalars() + threads.append((anybox_object, convo, actors)) + + return await templates.render_template( + db_session, + request, + "admin_direct_messages.html", + { + "threads": threads, + }, + ) + + @router.get("/outbox") async def admin_outbox( request: Request, diff --git a/app/templates/admin_direct_messages.html b/app/templates/admin_direct_messages.html new file mode 100644 index 0000000..5ab99d8 --- /dev/null +++ b/app/templates/admin_direct_messages.html @@ -0,0 +1,18 @@ +{%- import "utils.html" as utils with context -%} +{% extends "layout.html" %} + +{% block head %} +{{ local_actor.display_name }} - Direct messages +{% endblock %} + +{% block content %} + +{% for anybox_object, convo, actors in threads %} +
+ With {% for actor in actors %} + {{ actor.handle }} + {% endfor %} +
+ {{ utils.display_object(anybox_object) }} +{% endfor %} +{% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html index 0a89ae4..940378d 100644 --- a/app/templates/layout.html +++ b/app/templates/layout.html @@ -26,11 +26,11 @@