From 66a5dfb43343a0be15145c1fa17b198bca89cca2 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 30 Mar 2024 04:33:23 +0100 Subject: [PATCH] feat: add avatars to members --- foxnouns/blueprints/v2/members.py | 4 +++ foxnouns/models/member.py | 2 ++ foxnouns/tasks.py | 57 +++++++++++++++++++++++++++++-- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/foxnouns/blueprints/v2/members.py b/foxnouns/blueprints/v2/members.py index 7b4d21a..613fa35 100644 --- a/foxnouns/blueprints/v2/members.py +++ b/foxnouns/blueprints/v2/members.py @@ -2,6 +2,7 @@ from pydantic import Field from quart import Blueprint, g from quart_schema import validate_request, validate_response +from foxnouns import tasks from foxnouns.auth import require_auth from foxnouns.db import Member from foxnouns.db.aio import async_session @@ -54,4 +55,7 @@ async def create_member(data: MemberCreateModel): # we have to do it manually. await member.awaitable_attrs.user + if data.avatar: + tasks.process_member_avatar.delay(member.id, data.avatar) + return FullMemberModel.model_validate(member) diff --git a/foxnouns/models/member.py b/foxnouns/models/member.py index 1ab7162..9425a46 100644 --- a/foxnouns/models/member.py +++ b/foxnouns/models/member.py @@ -18,6 +18,8 @@ class MemberPatchModel(BasePatchModel): ) bio: str | None = Field(max_length=1024, default=None) + avatar: str | None = Field(max_length=1_000_000, default=None) + names: list[FieldEntry] = Field(default=[]) pronouns: list[PronounEntry] = Field(default=[]) fields: list[ProfileField] = Field(default=[]) diff --git a/foxnouns/tasks.py b/foxnouns/tasks.py index e6af598..9a56dcc 100644 --- a/foxnouns/tasks.py +++ b/foxnouns/tasks.py @@ -8,7 +8,7 @@ from celery.utils.log import get_task_logger from minio import Minio from sqlalchemy import select, update -from foxnouns.db import User +from foxnouns.db import Member, User from foxnouns.db.sync import session from foxnouns.settings import MINIO, REDIS_URL @@ -44,7 +44,7 @@ def convert_avatar(uri: str) -> bytes: @app.task -def process_user_avatar(user_id: int, avatar: str): +def process_user_avatar(user_id: int, avatar: str) -> None: with session() as conn: user = conn.scalar(select(User).where(User.id == user_id)) if not user: @@ -71,7 +71,7 @@ def process_user_avatar(user_id: int, avatar: str): @app.task -def delete_user_avatar(user_id: int): +def delete_user_avatar(user_id: int) -> None: with session() as conn: user = conn.scalar(select(User).where(User.id == user_id)) if not user: @@ -87,3 +87,54 @@ def delete_user_avatar(user_id: int): with session() as conn: conn.execute(update(User).values(avatar=None).where(User.id == user_id)) conn.commit() + + +@app.task +def process_member_avatar(member_id: int, avatar: str) -> None: + with session() as conn: + member = conn.scalar(select(Member).where(Member.id == member_id)) + if not member: + raise ValueError( + "process_member_avatar was passed the ID of a nonexistent member" + ) + + img = convert_avatar(avatar) + hash = hashlib.new("sha256", data=img).hexdigest() + old_hash = member.avatar + + minio.put_object( + bucket, + f"members/{member_id}/avatars/{hash}.webp", + BytesIO(img), + len(img), + "image/webp", + ) + + with session() as conn: + conn.execute(update(Member).values(avatar=hash).where(Member.id == member_id)) + conn.commit() + + if old_hash: + minio.remove_object(bucket, f"members/{member_id}/avatars/{old_hash}.webp") + + +@app.task +def delete_member_avatar(member_id: int) -> None: + with session() as conn: + member = conn.scalar(select(Member).where(Member.id == member_id)) + if not member: + raise ValueError( + "delete_member_avatar was passed the ID of a nonexistent member" + ) + if not member.avatar: + logger.info( + "delete_member_avatar was called for a member with a null avatar (%d)", + member_id, + ) + return + + minio.remove_object(bucket, f"members/{member_id}/avatars/{member.avatar}.webp") + + with session() as conn: + conn.execute(update(Member).values(avatar=None).where(Member.id == member_id)) + conn.commit()