microblog/app/admin.py

1248 lines
40 KiB
Python
Raw Normal View History

2022-08-11 20:07:40 +00:00
from datetime import datetime
2022-07-26 16:51:20 +00:00
import httpx
2022-06-22 18:11:22 +00:00
from fastapi import APIRouter
from fastapi import Cookie
from fastapi import Depends
from fastapi import Form
from fastapi import Request
from fastapi import UploadFile
from fastapi.exceptions import HTTPException
from fastapi.responses import RedirectResponse
2022-07-23 17:02:06 +00:00
from loguru import logger
2022-08-11 20:07:40 +00:00
from sqlalchemy import and_
2022-11-20 10:56:58 +00:00
from sqlalchemy import delete
2022-06-29 06:56:39 +00:00
from sqlalchemy import func
2022-08-11 20:07:40 +00:00
from sqlalchemy import or_
2022-06-29 06:56:39 +00:00
from sqlalchemy import select
2022-06-22 18:11:22 +00:00
from sqlalchemy.orm import joinedload
from app import activitypub as ap
from app import boxes
from app import models
from app import templates
from app.actor import LOCAL_ACTOR
2022-07-28 06:41:50 +00:00
from app.actor import fetch_actor
2022-06-22 18:11:22 +00:00
from app.actor import get_actors_metadata
from app.boxes import get_inbox_object_by_ap_id
from app.boxes import get_outbox_object_by_ap_id
2022-10-23 14:37:24 +00:00
from app.boxes import send_block
2022-06-22 18:11:22 +00:00
from app.boxes import send_follow
2022-10-23 14:37:24 +00:00
from app.boxes import send_unblock
2022-06-27 18:55:44 +00:00
from app.config import EMOJIS
2022-06-22 18:11:22 +00:00
from app.config import generate_csrf_token
from app.config import session_serializer
from app.config import verify_csrf_token
from app.config import verify_password
2022-06-29 18:43:17 +00:00
from app.database import AsyncSession
from app.database import get_db_session
2022-06-22 18:11:22 +00:00
from app.lookup import lookup
2022-09-04 07:24:58 +00:00
from app.templates import is_current_user_admin
2022-06-23 19:07:20 +00:00
from app.uploads import save_upload
2022-06-28 18:10:25 +00:00
from app.utils import pagination
2022-06-27 18:55:44 +00:00
from app.utils.emoji import EMOJIS_BY_NAME
2022-06-22 18:11:22 +00:00
async def user_session_or_redirect(
2022-06-22 18:11:22 +00:00
request: Request,
session: str | None = Cookie(default=None),
) -> None:
if request.method == "POST":
form_data = await request.form()
if "redirect_url" in form_data:
redirect_url = form_data["redirect_url"]
else:
redirect_url = request.url_for("admin_stream")
else:
redirect_url = str(request.url)
2022-06-22 18:11:22 +00:00
_RedirectToLoginPage = HTTPException(
status_code=302,
headers={"Location": request.url_for("login") + f"?redirect={redirect_url}"},
2022-06-22 18:11:22 +00:00
)
if not session:
2022-11-20 09:02:28 +00:00
logger.info("No existing admin session")
2022-06-22 18:11:22 +00:00
raise _RedirectToLoginPage
try:
loaded_session = session_serializer.loads(session, max_age=3600 * 24 * 3)
2022-06-22 18:11:22 +00:00
except Exception:
2022-11-20 09:02:28 +00:00
logger.exception("Failed to validate admin session")
2022-06-22 18:11:22 +00:00
raise _RedirectToLoginPage
if not loaded_session.get("is_logged_in"):
2022-11-20 09:02:28 +00:00
logger.info(f"Admin session invalidated: {loaded_session}")
2022-06-22 18:11:22 +00:00
raise _RedirectToLoginPage
return None
router = APIRouter(
dependencies=[Depends(user_session_or_redirect)],
)
unauthenticated_router = APIRouter()
@router.get("/lookup")
2022-06-29 18:43:17 +00:00
async def get_lookup(
2022-06-22 18:11:22 +00:00
request: Request,
query: str | None = None,
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-07-07 06:36:07 +00:00
) -> templates.TemplateResponse | RedirectResponse:
2022-07-26 16:51:20 +00:00
error = None
2022-06-22 18:11:22 +00:00
ap_object = None
actors_metadata = {}
if query:
2022-07-26 16:51:20 +00:00
try:
ap_object = await lookup(db_session, query)
except httpx.TimeoutException:
error = ap.FetchErrorTypeEnum.TIMEOUT
except (ap.ObjectNotFoundError, ap.ObjectIsGoneError):
error = ap.FetchErrorTypeEnum.NOT_FOUND
2022-09-12 06:04:16 +00:00
except (ap.ObjectUnavailableError):
error = ap.FetchErrorTypeEnum.UNAUHTORIZED
2022-07-26 16:51:20 +00:00
except Exception:
logger.exception(f"Failed to lookup {query}")
error = ap.FetchErrorTypeEnum.INTERNAL_ERROR
2022-06-22 18:11:22 +00:00
else:
2022-07-26 16:51:20 +00:00
if ap_object.ap_type in ap.ACTOR_TYPES:
try:
await fetch_actor(
db_session, ap_object.ap_id, save_if_not_found=False
)
2022-08-15 20:22:15 +00:00
except ap.ObjectNotFoundError:
pass
else:
return RedirectResponse(
request.url_for("admin_profile")
+ f"?actor_id={ap_object.ap_id}",
status_code=302,
)
2022-07-26 16:51:20 +00:00
actors_metadata = await get_actors_metadata(
db_session, [ap_object] # type: ignore
)
else:
# Check if the object is in the inbox
requested_object = await boxes.get_anybox_object_by_ap_id(
db_session, ap_object.ap_id
)
if requested_object:
return RedirectResponse(
2022-08-27 07:24:21 +00:00
request.url_for("admin_object")
+ f"?ap_id={ap_object.ap_id}#"
+ requested_object.permalink_id,
status_code=302,
)
2022-07-26 16:51:20 +00:00
actors_metadata = await get_actors_metadata(
db_session, [ap_object.actor] # type: ignore
)
2022-06-29 18:43:17 +00:00
return await templates.render_template(
db_session,
2022-06-22 18:11:22 +00:00
request,
"lookup.html",
{
"query": query,
"ap_object": ap_object,
"actors_metadata": actors_metadata,
2022-07-26 16:51:20 +00:00
"error": error,
2022-06-22 18:11:22 +00:00
},
)
@router.get("/new")
2022-06-29 18:43:17 +00:00
async def admin_new(
2022-06-22 18:11:22 +00:00
request: Request,
query: str | None = None,
2022-06-24 09:33:05 +00:00
in_reply_to: str | None = None,
2022-08-11 18:48:20 +00:00
with_content: str | None = None,
with_visibility: str | None = None,
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-22 18:11:22 +00:00
) -> templates.TemplateResponse:
content = ""
content_warning = None
2022-06-24 09:33:05 +00:00
in_reply_to_object = None
if in_reply_to:
2022-06-29 18:43:17 +00:00
in_reply_to_object = await boxes.get_anybox_object_by_ap_id(
db_session, in_reply_to
)
if not in_reply_to_object:
logger.info(f"Saving unknwown object {in_reply_to}")
raw_object = await ap.fetch(in_reply_to)
await boxes.save_object_to_inbox(db_session, raw_object)
await db_session.commit()
in_reply_to_object = await boxes.get_anybox_object_by_ap_id(
db_session, in_reply_to
)
# Add mentions to the initial note content
2022-06-24 09:33:05 +00:00
if not in_reply_to_object:
raise ValueError(f"Unknown object {in_reply_to=}")
if in_reply_to_object.actor.ap_id != LOCAL_ACTOR.ap_id:
content += f"{in_reply_to_object.actor.handle} "
for tag in in_reply_to_object.tags:
if tag.get("type") == "Mention" and tag["name"] != LOCAL_ACTOR.handle:
2022-07-28 06:41:50 +00:00
mentioned_actor = await fetch_actor(db_session, tag["href"])
content += f"{mentioned_actor.handle} "
2022-06-24 09:33:05 +00:00
# Copy the content warning if any
if in_reply_to_object.summary:
content_warning = in_reply_to_object.summary
2022-08-11 18:48:20 +00:00
elif with_content:
content += f"{with_content} "
2022-06-29 18:43:17 +00:00
return await templates.render_template(
db_session,
2022-06-22 18:11:22 +00:00
request,
"admin_new.html",
{
"in_reply_to_object": in_reply_to_object,
"content": content,
"content_warning": content_warning,
"visibility_choices": [
(v.name, ap.VisibilityEnum.get_display_name(v))
for v in ap.VisibilityEnum
],
2022-08-11 18:48:20 +00:00
"visibility": with_visibility,
2022-06-27 18:55:44 +00:00
"emojis": EMOJIS.split(" "),
"custom_emojis": sorted(
[dat for name, dat in EMOJIS_BY_NAME.items()],
key=lambda obj: obj["name"],
),
2022-06-22 18:11:22 +00:00
},
)
2022-06-26 09:09:43 +00:00
@router.get("/bookmarks")
2022-06-29 18:43:17 +00:00
async def admin_bookmarks(
2022-06-26 09:09:43 +00:00
request: Request,
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-26 09:09:43 +00:00
) -> templates.TemplateResponse:
2022-08-25 06:45:07 +00:00
# TODO: support pagination
2022-06-26 09:09:43 +00:00
stream = (
2022-06-29 18:43:17 +00:00
(
await db_session.scalars(
select(models.InboxObject)
.where(
models.InboxObject.ap_type.in_(
["Note", "Article", "Video", "Announce"]
),
models.InboxObject.is_bookmarked.is_(True),
2022-07-07 18:37:16 +00:00
models.InboxObject.is_deleted.is_(False),
2022-06-29 18:43:17 +00:00
)
2022-07-03 20:01:47 +00:00
.options(
joinedload(models.InboxObject.relates_to_inbox_object),
joinedload(models.InboxObject.relates_to_outbox_object).options(
joinedload(
models.OutboxObject.outbox_object_attachments
).options(joinedload(models.OutboxObjectAttachment.upload)),
),
joinedload(models.InboxObject.actor),
)
2022-06-29 18:43:17 +00:00
.order_by(models.InboxObject.ap_published_at.desc())
.limit(20)
2022-06-29 06:56:39 +00:00
)
2022-07-03 20:01:47 +00:00
)
.unique()
.all()
2022-06-26 09:09:43 +00:00
)
2022-06-29 18:43:17 +00:00
return await templates.render_template(
db_session,
2022-06-26 09:09:43 +00:00
request,
"admin_stream.html",
{
"stream": stream,
},
)
2022-07-07 18:37:16 +00:00
@router.get("/stream")
async def admin_stream(
request: Request,
db_session: AsyncSession = Depends(get_db_session),
cursor: str | None = None,
) -> templates.TemplateResponse:
where = [
models.InboxObject.is_hidden_from_stream.is_(False),
models.InboxObject.is_deleted.is_(False),
]
if cursor:
where.append(
models.InboxObject.ap_published_at < pagination.decode_cursor(cursor)
)
page_size = 20
remaining_count = await db_session.scalar(
select(func.count(models.InboxObject.id)).where(*where)
)
q = select(models.InboxObject).where(*where)
inbox = (
(
await db_session.scalars(
q.options(
joinedload(models.InboxObject.relates_to_inbox_object).options(
joinedload(models.InboxObject.actor)
),
joinedload(models.InboxObject.relates_to_outbox_object).options(
joinedload(
models.OutboxObject.outbox_object_attachments
).options(joinedload(models.OutboxObjectAttachment.upload)),
),
joinedload(models.InboxObject.actor),
)
.order_by(models.InboxObject.ap_published_at.desc())
.limit(20)
)
)
.unique()
.all()
)
next_cursor = (
pagination.encode_cursor(inbox[-1].ap_published_at)
if inbox and remaining_count > page_size
else None
)
actors_metadata = await get_actors_metadata(
db_session,
[
inbox_object.actor
for inbox_object in inbox
if inbox_object.ap_type == "Follow"
],
)
return await templates.render_template(
db_session,
request,
"admin_inbox.html",
{
"inbox": inbox,
"actors_metadata": actors_metadata,
"next_cursor": next_cursor,
"show_filters": False,
},
)
2022-06-25 06:23:28 +00:00
@router.get("/inbox")
2022-06-29 18:43:17 +00:00
async def admin_inbox(
2022-06-25 06:23:28 +00:00
request: Request,
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-28 18:10:25 +00:00
filter_by: str | None = None,
cursor: str | None = None,
2022-06-25 06:23:28 +00:00
) -> templates.TemplateResponse:
2022-07-05 18:47:00 +00:00
where = [
2022-07-07 18:37:16 +00:00
models.InboxObject.ap_type.not_in(
2022-08-06 07:32:22 +00:00
[
"Accept",
"Delete",
"Create",
"Update",
"Undo",
"Read",
2022-10-23 14:37:24 +00:00
"Reject",
2022-08-06 07:32:22 +00:00
"Add",
"Remove",
"EmojiReact",
]
2022-07-07 18:37:16 +00:00
),
models.InboxObject.is_deleted.is_(False),
2022-07-24 10:36:59 +00:00
models.InboxObject.is_transient.is_(False),
2022-07-05 18:47:00 +00:00
]
2022-06-28 18:10:25 +00:00
if filter_by:
2022-06-29 06:56:39 +00:00
where.append(models.InboxObject.ap_type == filter_by)
2022-06-28 18:10:25 +00:00
if cursor:
2022-06-29 06:56:39 +00:00
where.append(
2022-06-28 18:10:25 +00:00
models.InboxObject.ap_published_at < pagination.decode_cursor(cursor)
)
page_size = 20
2022-06-29 18:43:17 +00:00
remaining_count = await db_session.scalar(
select(func.count(models.InboxObject.id)).where(*where)
)
2022-06-29 06:56:39 +00:00
q = select(models.InboxObject).where(*where)
2022-06-28 18:10:25 +00:00
2022-06-25 06:23:28 +00:00
inbox = (
2022-06-29 18:43:17 +00:00
(
await db_session.scalars(
q.options(
2022-07-05 06:33:39 +00:00
joinedload(models.InboxObject.relates_to_inbox_object).options(
joinedload(models.InboxObject.actor)
),
2022-06-29 20:20:01 +00:00
joinedload(models.InboxObject.relates_to_outbox_object).options(
joinedload(
models.OutboxObject.outbox_object_attachments
).options(joinedload(models.OutboxObjectAttachment.upload)),
),
2022-06-29 18:43:17 +00:00
joinedload(models.InboxObject.actor),
)
.order_by(models.InboxObject.ap_published_at.desc())
.limit(20)
2022-06-29 06:56:39 +00:00
)
2022-06-25 06:23:28 +00:00
)
2022-06-29 06:56:39 +00:00
.unique()
2022-06-25 06:23:28 +00:00
.all()
)
2022-06-28 18:10:25 +00:00
next_cursor = (
pagination.encode_cursor(inbox[-1].ap_published_at)
if inbox and remaining_count > page_size
else None
)
2022-06-29 18:43:17 +00:00
actors_metadata = await get_actors_metadata(
db_session,
2022-06-28 19:10:22 +00:00
[
inbox_object.actor
for inbox_object in inbox
if inbox_object.ap_type == "Follow"
],
)
2022-06-29 18:43:17 +00:00
return await templates.render_template(
db_session,
2022-06-25 06:23:28 +00:00
request,
"admin_inbox.html",
{
"inbox": inbox,
2022-06-28 19:10:22 +00:00
"actors_metadata": actors_metadata,
2022-06-28 18:10:25 +00:00
"next_cursor": next_cursor,
2022-07-07 18:37:16 +00:00
"show_filters": True,
2022-06-25 06:23:28 +00:00
},
)
2022-08-11 20:07:40 +00:00
@router.get("/direct_messages")
async def admin_direct_messages(
request: Request,
db_session: AsyncSession = Depends(get_db_session),
cursor: str | None = None,
) -> templates.TemplateResponse:
2022-08-12 08:01:35 +00:00
# The process for building DMs thread is a bit compex in term of query
# but it does not require an extra tables to index/manage threads
2022-08-11 20:07:40 +00:00
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),
2022-08-12 08:01:35 +00:00
# Skip transient object like poll relies
models.InboxObject.is_transient.is_(False),
models.InboxObject.is_deleted.is_(False),
2022-08-11 20:07:40 +00:00
)
.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),
2022-08-12 08:01:35 +00:00
# Skip transient object like poll relies
models.OutboxObject.is_transient.is_(False),
models.OutboxObject.is_deleted.is_(False),
2022-08-11 20:07:40 +00:00
)
.group_by(models.OutboxObject.ap_context)
)
)
.unique()
.all()
)
2022-08-12 08:01:35 +00:00
# Build a "threads index" by combining objects from the inbox and outbox
2022-08-11 20:07:40 +00:00
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"],
)
2022-08-12 08:01:35 +00:00
# Fetch the latest object for each threads
2022-08-11 20:07:40 +00:00
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 = (
(
2022-08-13 13:53:07 +00:00
(
await db_session.scalars(
select(models.InboxObject)
.where(or_(*convos_with_last_from_inbox))
.options(
joinedload(models.InboxObject.actor),
)
2022-08-11 20:07:40 +00:00
)
)
2022-08-13 13:53:07 +00:00
.unique()
.all()
2022-08-11 20:07:40 +00:00
)
2022-08-13 13:53:07 +00:00
if convos_with_last_from_inbox
else []
2022-08-11 20:07:40 +00:00
)
last_from_outbox = (
(
2022-08-13 13:53:07 +00:00
(
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)),
)
2022-08-11 20:07:40 +00:00
)
)
2022-08-13 13:53:07 +00:00
.unique()
.all()
2022-08-11 20:07:40 +00:00
)
2022-08-13 13:53:07 +00:00
if convos_with_last_from_outbox
else []
2022-08-11 20:07:40 +00:00
)
2022-08-12 08:01:35 +00:00
# Build the template response
2022-08-11 20:07:40 +00:00
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,
},
)
2022-06-25 06:23:28 +00:00
@router.get("/outbox")
2022-06-29 18:43:17 +00:00
async def admin_outbox(
2022-06-28 18:10:25 +00:00
request: Request,
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-28 18:10:25 +00:00
filter_by: str | None = None,
cursor: str | None = None,
2022-06-25 06:23:28 +00:00
) -> templates.TemplateResponse:
2022-07-03 20:01:47 +00:00
where = [
2022-07-07 06:36:07 +00:00
models.OutboxObject.ap_type.not_in(["Accept", "Delete", "Update"]),
2022-07-03 20:01:47 +00:00
models.OutboxObject.is_deleted.is_(False),
2022-07-24 10:36:59 +00:00
models.OutboxObject.is_transient.is_(False),
2022-07-03 20:01:47 +00:00
]
if filter_by:
2022-06-29 06:56:39 +00:00
where.append(models.OutboxObject.ap_type == filter_by)
2022-06-28 18:10:25 +00:00
if cursor:
2022-06-29 06:56:39 +00:00
where.append(
2022-06-28 18:10:25 +00:00
models.OutboxObject.ap_published_at < pagination.decode_cursor(cursor)
)
page_size = 20
2022-06-29 18:43:17 +00:00
remaining_count = await db_session.scalar(
2022-06-29 06:56:39 +00:00
select(func.count(models.OutboxObject.id)).where(*where)
)
q = select(models.OutboxObject).where(*where)
2022-06-25 06:23:28 +00:00
outbox = (
2022-06-29 18:43:17 +00:00
(
await db_session.scalars(
q.options(
2022-07-03 20:42:14 +00:00
joinedload(models.OutboxObject.relates_to_inbox_object).options(
joinedload(models.InboxObject.actor),
),
2022-06-29 18:43:17 +00:00
joinedload(models.OutboxObject.relates_to_outbox_object),
joinedload(models.OutboxObject.relates_to_actor),
joinedload(models.OutboxObject.outbox_object_attachments).options(
joinedload(models.OutboxObjectAttachment.upload)
),
2022-06-29 18:43:17 +00:00
)
.order_by(models.OutboxObject.ap_published_at.desc())
.limit(page_size)
2022-06-29 06:56:39 +00:00
)
2022-06-25 06:23:28 +00:00
)
2022-06-29 06:56:39 +00:00
.unique()
2022-06-25 06:23:28 +00:00
.all()
)
2022-06-28 18:10:25 +00:00
next_cursor = (
pagination.encode_cursor(outbox[-1].ap_published_at)
if outbox and remaining_count > page_size
else None
)
2022-06-29 18:43:17 +00:00
actors_metadata = await get_actors_metadata(
db_session,
[
outbox_object.relates_to_actor
for outbox_object in outbox
if outbox_object.relates_to_actor
],
)
2022-06-29 18:43:17 +00:00
return await templates.render_template(
db_session,
2022-06-25 06:23:28 +00:00
request,
"admin_outbox.html",
{
"actors_metadata": actors_metadata,
2022-06-25 06:23:28 +00:00
"outbox": outbox,
2022-06-28 18:10:25 +00:00
"next_cursor": next_cursor,
2022-06-25 06:23:28 +00:00
},
)
2022-06-22 18:11:22 +00:00
@router.get("/notifications")
2022-06-29 18:43:17 +00:00
async def get_notifications(
2022-08-25 06:45:07 +00:00
request: Request,
db_session: AsyncSession = Depends(get_db_session),
cursor: str | None = None,
2022-06-22 18:11:22 +00:00
) -> templates.TemplateResponse:
2022-08-25 06:45:07 +00:00
where = []
if cursor:
decoded_cursor = pagination.decode_cursor(cursor)
where.append(models.Notification.created_at < decoded_cursor)
page_size = 20
remaining_count = await db_session.scalar(
select(func.count(models.Notification.id)).where(*where)
)
2022-06-22 18:11:22 +00:00
notifications = (
2022-06-29 18:43:17 +00:00
(
await db_session.scalars(
select(models.Notification)
2022-08-25 06:45:07 +00:00
.where(*where)
2022-06-29 18:43:17 +00:00
.options(
joinedload(models.Notification.actor),
joinedload(models.Notification.inbox_object).options(
joinedload(models.InboxObject.actor)
),
joinedload(models.Notification.outbox_object).options(
joinedload(
models.OutboxObject.outbox_object_attachments
).options(joinedload(models.OutboxObjectAttachment.upload)),
),
joinedload(models.Notification.webmention),
2022-06-29 18:43:17 +00:00
)
.order_by(models.Notification.created_at.desc())
2022-08-25 06:45:07 +00:00
.limit(page_size)
2022-06-29 06:56:39 +00:00
)
2022-06-22 18:11:22 +00:00
)
2022-06-29 06:56:39 +00:00
.unique()
2022-06-22 18:11:22 +00:00
.all()
)
2022-06-29 18:43:17 +00:00
actors_metadata = await get_actors_metadata(
db_session, [notif.actor for notif in notifications if notif.actor]
2022-06-22 18:11:22 +00:00
)
2022-08-25 06:45:07 +00:00
more_unread_count = 0
next_cursor = None
2022-11-20 09:13:17 +00:00
2022-08-25 06:45:07 +00:00
if notifications and remaining_count > page_size:
decoded_next_cursor = notifications[-1].created_at
next_cursor = pagination.encode_cursor(decoded_next_cursor)
# If on the "see more" page there's more unread notification, we want
# to display it next to the link
more_unread_count = await db_session.scalar(
select(func.count(models.Notification.id)).where(
models.Notification.is_new.is_(True),
models.Notification.created_at < decoded_next_cursor,
)
)
2022-11-20 09:13:17 +00:00
# Render the template before we change the new flag on notifications
tpl_resp = await templates.render_template(
2022-06-29 18:43:17 +00:00
db_session,
2022-06-22 18:11:22 +00:00
request,
"notifications.html",
{
"notifications": notifications,
"actors_metadata": actors_metadata,
2022-08-25 06:45:07 +00:00
"next_cursor": next_cursor,
"more_unread_count": more_unread_count,
2022-06-22 18:11:22 +00:00
},
)
2022-11-20 09:13:17 +00:00
if len({notif.id for notif in notifications if notif.is_new}):
for notif in notifications:
notif.is_new = False
await db_session.commit()
return tpl_resp
2022-06-22 18:11:22 +00:00
2022-06-25 10:19:12 +00:00
@router.get("/object")
2022-06-29 18:43:17 +00:00
async def admin_object(
2022-06-25 10:19:12 +00:00
request: Request,
ap_id: str,
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-25 10:19:12 +00:00
) -> templates.TemplateResponse:
2022-06-29 18:43:17 +00:00
requested_object = await boxes.get_anybox_object_by_ap_id(db_session, ap_id)
2022-09-07 17:45:34 +00:00
if not requested_object or requested_object.is_deleted:
2022-06-25 10:29:35 +00:00
raise HTTPException(status_code=404)
replies_tree = await boxes.get_replies_tree(
db_session,
requested_object,
is_current_user_admin=True,
)
2022-06-25 10:29:35 +00:00
2022-06-29 18:43:17 +00:00
return await templates.render_template(
db_session,
2022-06-25 10:19:12 +00:00
request,
2022-06-25 10:29:35 +00:00
"object.html",
{"replies_tree": replies_tree},
2022-06-25 10:19:12 +00:00
)
@router.get("/profile")
2022-06-29 18:43:17 +00:00
async def admin_profile(
2022-06-25 10:19:12 +00:00
request: Request,
actor_id: str,
2022-08-26 06:10:46 +00:00
cursor: str | None = None,
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-25 10:19:12 +00:00
) -> templates.TemplateResponse:
2022-08-26 06:10:46 +00:00
# TODO: show featured/pinned
2022-06-29 18:43:17 +00:00
actor = (
await db_session.execute(
select(models.Actor).where(models.Actor.ap_id == actor_id)
)
2022-06-29 06:56:39 +00:00
).scalar_one_or_none()
2022-06-25 10:19:12 +00:00
if not actor:
raise HTTPException(status_code=404)
2022-06-29 18:43:17 +00:00
actors_metadata = await get_actors_metadata(db_session, [actor])
2022-06-25 10:19:12 +00:00
2022-08-26 06:10:46 +00:00
where = [
models.InboxObject.is_deleted.is_(False),
models.InboxObject.actor_id == actor.id,
models.InboxObject.ap_type.in_(
["Note", "Article", "Video", "Page", "Announce"]
),
]
if cursor:
decoded_cursor = pagination.decode_cursor(cursor)
where.append(models.InboxObject.ap_published_at < decoded_cursor)
page_size = 20
remaining_count = await db_session.scalar(
select(func.count(models.InboxObject.id)).where(*where)
)
2022-06-29 18:43:17 +00:00
inbox_objects = (
2022-07-27 16:58:57 +00:00
(
await db_session.scalars(
select(models.InboxObject)
2022-08-26 06:10:46 +00:00
.where(*where)
2022-07-27 16:58:57 +00:00
.options(
2022-07-27 17:04:25 +00:00
joinedload(models.InboxObject.relates_to_inbox_object).options(
joinedload(models.InboxObject.actor)
),
2022-07-27 16:58:57 +00:00
joinedload(models.InboxObject.relates_to_outbox_object).options(
joinedload(
models.OutboxObject.outbox_object_attachments
).options(joinedload(models.OutboxObjectAttachment.upload)),
),
joinedload(models.InboxObject.actor),
)
.order_by(models.InboxObject.ap_published_at.desc())
2022-08-26 06:10:46 +00:00
.limit(page_size)
2022-06-29 18:43:17 +00:00
)
2022-06-25 10:19:12 +00:00
)
2022-07-27 16:58:57 +00:00
.unique()
.all()
)
2022-06-25 10:19:12 +00:00
2022-08-26 06:10:46 +00:00
next_cursor = (
pagination.encode_cursor(inbox_objects[-1].created_at)
if inbox_objects and remaining_count > page_size
else None
)
2022-06-29 18:43:17 +00:00
return await templates.render_template(
db_session,
2022-06-25 10:19:12 +00:00
request,
"admin_profile.html",
{
"actors_metadata": actors_metadata,
"actor": actor,
"inbox_objects": inbox_objects,
2022-08-26 06:10:46 +00:00
"next_cursor": next_cursor,
2022-06-25 10:19:12 +00:00
},
)
@router.post("/actions/force_delete")
async def admin_actions_force_delete(
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
ap_object_to_delete = await get_inbox_object_by_ap_id(db_session, ap_object_id)
if not ap_object_to_delete:
raise ValueError(f"Cannot find {ap_object_id}")
logger.info(f"Deleting {ap_object_to_delete.ap_type}/{ap_object_to_delete.ap_id}")
await boxes._revert_side_effect_for_deleted_object(
db_session,
None,
ap_object_to_delete,
None,
)
ap_object_to_delete.is_deleted = True
await db_session.commit()
return RedirectResponse(redirect_url, status_code=302)
2022-11-20 10:56:58 +00:00
@router.post("/actions/force_delete_webmention")
async def admin_actions_force_delete_webmention(
request: Request,
webmention_id: int = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
webmention = await boxes.get_webmention_by_id(db_session, webmention_id)
if not webmention:
raise ValueError(f"Cannot find {webmention_id}")
if not webmention.outbox_object:
raise ValueError(f"Missing related outbox object for {webmention_id}")
# TODO: move this
logger.info(f"Deleting {webmention_id}")
webmention.is_deleted = True
await db_session.flush()
from app.webmentions import _handle_webmention_side_effects
await _handle_webmention_side_effects(
db_session, webmention, webmention.outbox_object
)
# Delete related notifications
notif_deletion_result = await db_session.execute(
delete(models.Notification)
.where(models.Notification.webmention_id == webmention.id)
.execution_options(synchronize_session=False)
)
logger.info(
f"Deleted {notif_deletion_result.rowcount} notifications" # type: ignore
)
await db_session.commit()
return RedirectResponse(redirect_url, status_code=302)
2022-06-22 18:11:22 +00:00
@router.post("/actions/follow")
2022-06-29 18:43:17 +00:00
async def admin_actions_follow(
2022-06-22 18:11:22 +00:00
request: Request,
ap_actor_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-22 18:11:22 +00:00
) -> RedirectResponse:
2022-07-31 08:35:11 +00:00
logger.info(f"Following {ap_actor_id}")
2022-06-29 18:43:17 +00:00
await send_follow(db_session, ap_actor_id)
2022-06-22 18:11:22 +00:00
return RedirectResponse(redirect_url, status_code=302)
2022-07-31 08:35:11 +00:00
@router.post("/actions/block")
async def admin_actions_block(
request: Request,
ap_actor_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
2022-10-23 14:37:24 +00:00
await send_block(db_session, ap_actor_id)
2022-07-31 08:35:11 +00:00
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/unblock")
async def admin_actions_unblock(
request: Request,
ap_actor_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
logger.info(f"Unblocking {ap_actor_id}")
2022-10-23 14:37:24 +00:00
await send_unblock(db_session, ap_actor_id)
2022-07-31 08:35:11 +00:00
return RedirectResponse(redirect_url, status_code=302)
2022-07-02 08:33:20 +00:00
@router.post("/actions/delete")
async def admin_actions_delete(
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
await boxes.send_delete(db_session, ap_object_id)
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/accept_incoming_follow")
async def admin_actions_accept_incoming_follow(
request: Request,
notification_id: int = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
await boxes.send_accept(db_session, notification_id)
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/reject_incoming_follow")
async def admin_actions_reject_incoming_follow(
request: Request,
notification_id: int = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
await boxes.send_reject(db_session, notification_id)
return RedirectResponse(redirect_url, status_code=302)
2022-06-22 18:11:22 +00:00
@router.post("/actions/like")
2022-06-29 18:43:17 +00:00
async def admin_actions_like(
2022-06-22 18:11:22 +00:00
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-22 18:11:22 +00:00
) -> RedirectResponse:
2022-06-29 18:43:17 +00:00
await boxes.send_like(db_session, ap_object_id)
2022-06-22 18:11:22 +00:00
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/undo")
2022-06-29 18:43:17 +00:00
async def admin_actions_undo(
2022-06-22 18:11:22 +00:00
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-22 18:11:22 +00:00
) -> RedirectResponse:
2022-06-29 18:43:17 +00:00
await boxes.send_undo(db_session, ap_object_id)
2022-06-22 18:11:22 +00:00
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/announce")
2022-06-29 18:43:17 +00:00
async def admin_actions_announce(
2022-06-22 18:11:22 +00:00
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-22 18:11:22 +00:00
) -> RedirectResponse:
2022-06-29 18:43:17 +00:00
await boxes.send_announce(db_session, ap_object_id)
2022-06-22 18:11:22 +00:00
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/bookmark")
2022-06-29 18:43:17 +00:00
async def admin_actions_bookmark(
2022-06-22 18:11:22 +00:00
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-22 18:11:22 +00:00
) -> RedirectResponse:
2022-06-29 18:43:17 +00:00
inbox_object = await get_inbox_object_by_ap_id(db_session, ap_object_id)
2022-06-22 18:11:22 +00:00
if not inbox_object:
logger.info(f"Saving unknwown object {ap_object_id}")
raw_object = await ap.fetch(ap_object_id)
inbox_object = await boxes.save_object_to_inbox(db_session, raw_object)
2022-06-22 18:11:22 +00:00
inbox_object.is_bookmarked = True
2022-06-29 18:43:17 +00:00
await db_session.commit()
2022-06-22 18:11:22 +00:00
return RedirectResponse(redirect_url, status_code=302)
2022-06-26 09:09:43 +00:00
@router.post("/actions/unbookmark")
2022-06-29 18:43:17 +00:00
async def admin_actions_unbookmark(
2022-06-26 09:09:43 +00:00
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-26 09:09:43 +00:00
) -> RedirectResponse:
2022-06-29 18:43:17 +00:00
inbox_object = await get_inbox_object_by_ap_id(db_session, ap_object_id)
2022-06-26 09:09:43 +00:00
if not inbox_object:
raise ValueError("Should never happen")
inbox_object.is_bookmarked = False
2022-06-29 18:43:17 +00:00
await db_session.commit()
2022-06-26 09:09:43 +00:00
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/pin")
2022-06-29 18:43:17 +00:00
async def admin_actions_pin(
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
2022-06-29 18:43:17 +00:00
outbox_object = await get_outbox_object_by_ap_id(db_session, ap_object_id)
if not outbox_object:
raise ValueError("Should never happen")
outbox_object.is_pinned = True
2022-06-29 18:43:17 +00:00
await db_session.commit()
return RedirectResponse(redirect_url, status_code=302)
@router.post("/actions/unpin")
2022-06-29 18:43:17 +00:00
async def admin_actions_unpin(
request: Request,
ap_object_id: str = Form(),
redirect_url: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
2022-06-29 18:43:17 +00:00
outbox_object = await get_outbox_object_by_ap_id(db_session, ap_object_id)
if not outbox_object:
raise ValueError("Should never happen")
outbox_object.is_pinned = False
2022-06-29 18:43:17 +00:00
await db_session.commit()
return RedirectResponse(redirect_url, status_code=302)
2022-06-22 18:11:22 +00:00
@router.post("/actions/new")
2022-06-29 18:43:17 +00:00
async def admin_actions_new(
2022-06-22 18:11:22 +00:00
request: Request,
2022-06-27 18:55:44 +00:00
files: list[UploadFile] = [],
content: str | None = Form(None),
2022-06-22 18:11:22 +00:00
redirect_url: str = Form(),
2022-06-24 20:41:43 +00:00
in_reply_to: str | None = Form(None),
2022-06-28 19:10:22 +00:00
content_warning: str | None = Form(None),
is_sensitive: bool = Form(False),
visibility: str = Form(),
2022-07-24 10:36:59 +00:00
poll_type: str | None = Form(None),
2022-07-25 20:51:53 +00:00
name: str | None = Form(None),
2022-06-22 18:11:22 +00:00
csrf_check: None = Depends(verify_csrf_token),
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-06-22 18:11:22 +00:00
) -> RedirectResponse:
if not content and not content_warning:
raise HTTPException(status_code=422, detail="Error: object must have a content")
# Do like Mastodon, if there's only a CW with no content and some attachments,
# swap the CW and the content
if not content and content_warning and len(files) >= 1:
content = content_warning
is_sensitive = True
content_warning = None
if not content:
raise HTTPException(status_code=422, detail="Error: objec must have a content")
2022-06-22 18:11:22 +00:00
# XXX: for some reason, no files restuls in an empty single file
2022-06-23 19:07:20 +00:00
uploads = []
2022-07-21 20:43:06 +00:00
raw_form_data = await request.form()
2022-06-22 18:11:22 +00:00
if len(files) >= 1 and files[0].filename:
2022-06-23 19:07:20 +00:00
for f in files:
2022-06-29 18:43:17 +00:00
upload = await save_upload(db_session, f)
2022-07-21 20:43:06 +00:00
uploads.append((upload, f.filename, raw_form_data.get("alt_" + f.filename)))
2022-07-24 10:36:59 +00:00
ap_type = "Note"
poll_duration_in_minutes = None
2022-07-24 14:12:55 +00:00
poll_answers = None
2022-07-24 10:36:59 +00:00
if poll_type:
ap_type = "Question"
2022-07-24 14:12:55 +00:00
poll_answers = []
2022-07-24 10:36:59 +00:00
for i in ["1", "2", "3", "4"]:
if answer := raw_form_data.get(f"poll_answer_{i}"):
2022-07-24 14:12:55 +00:00
poll_answers.append(answer)
2022-07-24 10:36:59 +00:00
2022-07-24 14:12:55 +00:00
if not poll_answers or len(poll_answers) < 2:
2022-07-24 10:36:59 +00:00
raise ValueError("Question must have at least 2 answers")
poll_duration_in_minutes = int(raw_form_data["poll_duration"])
2022-07-25 20:51:53 +00:00
elif name:
ap_type = "Article"
2022-07-24 10:36:59 +00:00
2022-06-29 18:43:17 +00:00
public_id = await boxes.send_create(
db_session,
2022-07-24 10:36:59 +00:00
ap_type=ap_type,
2022-06-24 09:33:05 +00:00
source=content,
uploads=uploads,
in_reply_to=in_reply_to or None,
visibility=ap.VisibilityEnum[visibility],
2022-06-28 19:10:22 +00:00
content_warning=content_warning or None,
is_sensitive=True if content_warning else is_sensitive,
2022-07-24 10:36:59 +00:00
poll_type=poll_type,
2022-07-24 14:12:55 +00:00
poll_answers=poll_answers,
2022-07-24 10:36:59 +00:00
poll_duration_in_minutes=poll_duration_in_minutes,
2022-07-25 20:51:53 +00:00
name=name,
2022-06-24 09:33:05 +00:00
)
2022-06-22 18:11:22 +00:00
return RedirectResponse(
request.url_for("outbox_by_public_id", public_id=public_id),
status_code=302,
)
2022-07-23 17:02:06 +00:00
@router.post("/actions/vote")
async def admin_actions_vote(
request: Request,
redirect_url: str = Form(),
in_reply_to: str = Form(),
csrf_check: None = Depends(verify_csrf_token),
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse:
form_data = await request.form()
names = form_data.getlist("name")
logger.info(f"{names=}")
2022-07-23 21:06:30 +00:00
await boxes.send_vote(
db_session,
in_reply_to=in_reply_to,
names=names,
)
2022-07-23 17:02:06 +00:00
return RedirectResponse(redirect_url, status_code=302)
2022-06-22 18:11:22 +00:00
@unauthenticated_router.get("/login")
2022-06-29 18:43:17 +00:00
async def login(
2022-06-22 18:11:22 +00:00
request: Request,
2022-06-29 18:43:17 +00:00
db_session: AsyncSession = Depends(get_db_session),
2022-09-04 07:24:58 +00:00
) -> templates.TemplateResponse | RedirectResponse:
if is_current_user_admin(request):
return RedirectResponse(request.url_for("admin_stream"), status_code=302)
2022-06-29 18:43:17 +00:00
return await templates.render_template(
db_session,
2022-06-22 18:11:22 +00:00
request,
"login.html",
2022-07-10 09:04:28 +00:00
{
"csrf_token": generate_csrf_token(),
"redirect": request.query_params.get("redirect", ""),
},
2022-06-22 18:11:22 +00:00
)
@unauthenticated_router.post("/login")
2022-06-29 18:43:17 +00:00
async def login_validation(
2022-06-22 18:11:22 +00:00
request: Request,
password: str = Form(),
2022-07-14 18:05:36 +00:00
redirect: str | None = Form(None),
2022-06-22 18:11:22 +00:00
csrf_check: None = Depends(verify_csrf_token),
2022-08-22 16:50:20 +00:00
db_session: AsyncSession = Depends(get_db_session),
) -> RedirectResponse | templates.TemplateResponse:
2022-06-22 18:11:22 +00:00
if not verify_password(password):
2022-08-22 16:50:20 +00:00
logger.warning("Invalid password")
return await templates.render_template(
db_session,
request,
"login.html",
{
"error": "Invalid password",
"csrf_token": generate_csrf_token(),
"redirect": request.query_params.get("redirect", ""),
},
status_code=403,
)
2022-06-22 18:11:22 +00:00
2022-09-04 07:24:58 +00:00
resp = RedirectResponse(
redirect or request.url_for("admin_stream"), status_code=302
)
2022-06-22 18:11:22 +00:00
resp.set_cookie("session", session_serializer.dumps({"is_logged_in": True})) # type: ignore # noqa: E501
return resp
@router.get("/logout")
2022-06-29 18:43:17 +00:00
async def logout(
2022-06-22 18:11:22 +00:00
request: Request,
) -> RedirectResponse:
resp = RedirectResponse(request.url_for("index"), status_code=302)
resp.set_cookie("session", session_serializer.dumps({"is_logged_in": False})) # type: ignore # noqa: E501
return resp