From f512089fd9f71313b15c17c62f85d11ee38d5fa5 Mon Sep 17 00:00:00 2001 From: htylight Date: Thu, 27 Jul 2023 18:19:21 +0800 Subject: [PATCH] implemented apply friend --- migrations/env.py | 2 + ...reate_table_user_account_user_profile_.py} | 22 ++- ..._change_inviter_column_to_grou_chat_id_.py | 30 ++++ src/crud/apply_crud.py | 71 ++++++++ src/crud/multitable_crud.py | 43 +++++ src/crud/user_crud.py | 153 ++++++++++++------ src/database/json_typeddict.py | 11 ++ src/database/models.py | 101 ++++++++---- src/main.py | 19 ++- src/response_models/search_response.py | 5 + src/response_models/user_response.py | 17 ++ src/routers/apply.py | 93 +++++++++++ src/routers/search.py | 22 +++ src/routers/signin.py | 6 +- src/routers/signup.py | 2 +- src/routers/user_account.py | 15 +- src/routers/user_profile.py | 41 +++-- src/utils/static_file.py | 55 ++++++- 18 files changed, 587 insertions(+), 121 deletions(-) rename migrations/versions/{3a1f57242769_create_user_account_user_profile_and_.py => c2ce366349b6_create_table_user_account_user_profile_.py} (73%) mode change 100755 => 100644 create mode 100644 migrations/versions/d0c7f4dd4894_change_inviter_column_to_grou_chat_id_.py create mode 100644 src/crud/apply_crud.py create mode 100644 src/crud/multitable_crud.py create mode 100644 src/database/json_typeddict.py create mode 100644 src/response_models/search_response.py create mode 100644 src/routers/apply.py create mode 100644 src/routers/search.py diff --git a/migrations/env.py b/migrations/env.py index ef1a578..fbc6aa8 100755 --- a/migrations/env.py +++ b/migrations/env.py @@ -46,6 +46,8 @@ def run_migrations_offline() -> None: target_metadata=target_metadata, literal_binds=True, dialect_opts={"paramstyle": "named"}, + compare_type=True, + compare_server_default=True, ) with context.begin_transaction(): diff --git a/migrations/versions/3a1f57242769_create_user_account_user_profile_and_.py b/migrations/versions/c2ce366349b6_create_table_user_account_user_profile_.py old mode 100755 new mode 100644 similarity index 73% rename from migrations/versions/3a1f57242769_create_user_account_user_profile_and_.py rename to migrations/versions/c2ce366349b6_create_table_user_account_user_profile_.py index 5ef4032..767bdad --- a/migrations/versions/3a1f57242769_create_user_account_user_profile_and_.py +++ b/migrations/versions/c2ce366349b6_create_table_user_account_user_profile_.py @@ -1,8 +1,8 @@ -"""create user_account, user_profile and contact table +"""create table user_account, user_profile, contact and apply -Revision ID: 3a1f57242769 +Revision ID: c2ce366349b6 Revises: -Create Date: 2023-07-07 21:55:20.913012 +Create Date: 2023-07-22 20:42:45.268914 """ from alembic import op @@ -10,7 +10,7 @@ import sqlalchemy as sa from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. -revision = '3a1f57242769' +revision = 'c2ce366349b6' down_revision = None branch_labels = None depends_on = None @@ -18,6 +18,17 @@ depends_on = None def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### + op.create_table('apply', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('relation', sa.Integer(), nullable=False), + sa.Column('applicant', sa.String(length=26), nullable=False), + sa.Column('recipient', sa.String(length=26), nullable=False), + sa.Column('inviter', sa.String(length=26), nullable=True), + sa.Column('hello', sa.String(), nullable=False), + sa.Column('setting', postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) op.create_table('user_account', sa.Column('id', sa.String(length=26), nullable=False), sa.Column('username', sa.String(length=20), nullable=False), @@ -46,7 +57,7 @@ def upgrade() -> None: sa.Column('location', sa.String(), nullable=True), sa.Column('status', sa.String(), nullable=True), sa.Column('sign', sa.String(), nullable=True), - sa.Column('avatars', sa.String(), nullable=True), + sa.Column('avatar', sa.String(), nullable=True), sa.Column('user_id', sa.String(length=26), nullable=False), sa.ForeignKeyConstraint(['user_id'], ['user_account.id'], ondelete='CASCADE'), sa.PrimaryKeyConstraint('id') @@ -59,4 +70,5 @@ def downgrade() -> None: op.drop_table('user_profile') op.drop_table('contact') op.drop_table('user_account') + op.drop_table('apply') # ### end Alembic commands ### diff --git a/migrations/versions/d0c7f4dd4894_change_inviter_column_to_grou_chat_id_.py b/migrations/versions/d0c7f4dd4894_change_inviter_column_to_grou_chat_id_.py new file mode 100644 index 0000000..cd44d85 --- /dev/null +++ b/migrations/versions/d0c7f4dd4894_change_inviter_column_to_grou_chat_id_.py @@ -0,0 +1,30 @@ +"""change inviter column to grou_chat_id of apply table + +Revision ID: d0c7f4dd4894 +Revises: c2ce366349b6 +Create Date: 2023-07-23 18:32:31.233737 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'd0c7f4dd4894' +down_revision = 'c2ce366349b6' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('apply', sa.Column('group_chat_id', sa.String(length=26), nullable=True)) + op.drop_column('apply', 'inviter') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('apply', sa.Column('inviter', sa.VARCHAR(length=26), autoincrement=False, nullable=True)) + op.drop_column('apply', 'group_chat_id') + # ### end Alembic commands ### diff --git a/src/crud/apply_crud.py b/src/crud/apply_crud.py new file mode 100644 index 0000000..067fc40 --- /dev/null +++ b/src/crud/apply_crud.py @@ -0,0 +1,71 @@ +from typing import Literal, Tuple, Union +from datetime import date + +from sqlalchemy import select, delete, update, insert +from sqlalchemy import ScalarResult + +from sqlalchemy.exc import NoResultFound + +from ..database.db import async_session +from ..database.models import Apply + + +async def insert_apply( + relation: int, + applicant: str, + recipient: str, + group_chat_id: str | None, + hello: str, + setting: dict[str, str], +): + session = async_session() + res: ScalarResult[Apply] = await session.scalars( + select(Apply).where(Apply.recipient == recipient, Apply.applicant == applicant) + ) + + try: + apply = res.one() + apply.hello = hello + apply.setting = setting + session.add(apply) + except NoResultFound: + apply = Apply( + relation=relation, + applicant=applicant, + recipient=recipient, + group_chat_id=group_chat_id, + hello=hello, + setting=setting, + ) + session.add(apply) + finally: + await session.commit() + await session.close() + + +async def select_apply_all(recipient: str) -> list[Apply]: + session = async_session() + res: ScalarResult[Apply] = await session.scalars( + select(Apply) + .where(Apply.recipient == recipient) + .order_by(Apply.created_at.desc()) + ) + try: + return list(res.all()) + except NoResultFound: + return [] + finally: + await session.close() + + +async def delete_apply(relation: int, applicant: str, recipient: str): + session = async_session() + await session.execute( + delete(Apply).where( + Apply.relation == relation, + Apply.applicant == applicant, + Apply.recipient == recipient, + ) + ) + await session.commit() + await session.close() diff --git a/src/crud/multitable_crud.py b/src/crud/multitable_crud.py new file mode 100644 index 0000000..78b0959 --- /dev/null +++ b/src/crud/multitable_crud.py @@ -0,0 +1,43 @@ +from sqlalchemy import select, delete, update +from sqlalchemy import Result, ScalarResult, or_ +from sqlalchemy.orm.attributes import flag_modified + +from ..database.db import async_session +from ..database.models import * + + +async def insert_contact_friend( + relation: int, + applicant: str, + recipient: str, + applicant_setting: dict, + recipient_setting: dict, +): + session = async_session() + try: + await session.execute( + delete(Apply).where( + Apply.recipient == recipient, + Apply.applicant == applicant, + Apply.relation == relation, + ) + ) + res: ScalarResult[Contact] = await session.scalars( + select(Contact).where( + or_(Contact.user_id == applicant, Contact.user_id == recipient) + ) + ) + for contact in res.all(): + if contact.user_id == recipient: + contact.friends[applicant] = recipient_setting + flag_modified(contact, "friends") + else: + contact.friends[recipient] = applicant_setting + flag_modified(contact, "friends") + + session.add_all(res.all()) + await session.commit() + except Exception: + raise Exception + finally: + await session.close() diff --git a/src/crud/user_crud.py b/src/crud/user_crud.py index 0a36460..b757917 100755 --- a/src/crud/user_crud.py +++ b/src/crud/user_crud.py @@ -1,40 +1,22 @@ -from typing import Literal, Tuple +from typing import Literal, Tuple, Union from datetime import date -from sqlalchemy import select, update +from sqlalchemy import select, update, Row, Result, Sequence from sqlalchemy import ScalarResult import ulid +from sqlalchemy.exc import NoResultFound from ..database.db import async_session from ..database.models import UserAccount, UserProfile, Contact -async def select_user_by(condition: Literal['email', 'username', 'id'], value: str) -> Tuple[bool, UserAccount]: - session = async_session() - res: ScalarResult[UserAccount] = ScalarResult[UserAccount] - match condition: - case 'email': - res = await session.scalars(select(UserAccount).where(UserAccount.email == value)) - case 'username': - res = await session.scalars(select(UserAccount).where(UserAccount.username == value)) - case 'id': - res = await session.scalars(select(UserAccount).where(UserAccount.id == value)) - - user = res.first() - - return (True, user) if user else (False, None) - - async def insert_user(username: str, password: str, email: str): session = async_session() id = ulid.new().str user = UserAccount(id=id, username=username, password=password, email=email) profile = UserProfile(nickname=username) - contact = Contact( - friends={id: {'friendRemark': None, 'friendGroup': '我的好友'}}, friend_groups=['我的好友'], - group_chats={} - ) + contact = Contact(friends={id: {}}, friend_groups=["我的好友"], group_chats={}) user.profile = profile user.contact = contact session.add(user) @@ -42,20 +24,78 @@ async def insert_user(username: str, password: str, email: str): await session.close() +async def select_account_by( + condition: Literal["email", "username", "id"], value: str +) -> Tuple[bool, UserAccount]: + session = async_session() + res: ScalarResult[UserAccount] = ScalarResult[UserAccount] + match condition: + case "email": + res = await session.scalars( + select(UserAccount).where(UserAccount.email == value) + ) + case "username": + res = await session.scalars( + select(UserAccount).where(UserAccount.username == value) + ) + case "id": + res = await session.scalars( + select(UserAccount).where(UserAccount.id == value) + ) + + user = res.first() + + return (True, user) if user else (False, None) + + +async def update_account( + account: Literal["username", "email", "password"], id: str, value: str +): + session = async_session() + match account: + case "username": + await session.execute( + update(UserAccount).where(UserAccount.id == id).values(username=value) + ) + case "email": + await session.execute( + update(UserAccount).where(UserAccount.id == id).values(email=value) + ) + case "password": + await session.execute( + update(UserAccount).where(UserAccount.id == id).values(password=value) + ) + await session.commit() + await session.close() + + async def select_profile(id: str) -> UserProfile: session = async_session() - res: ScalarResult[UserProfile] = await session.scalars(select(UserProfile).where(UserProfile.user_id == id)) + res: ScalarResult[UserProfile] = await session.scalars( + select(UserProfile).where(UserProfile.user_id == id) + ) res: UserProfile = res.first() await session.close() return res -async def update_profile_basic(id: str, nickname: str, location: str, birthday: str, gender: str, ): +async def update_profile_basic( + id: str, + nickname: str, + location: str, + birthday: str, + gender: str, +): session = async_session() await session.execute( update(UserProfile) .where(UserProfile.user_id == id) - .values(nickname=nickname, location=location, birthday=date.fromisoformat(birthday), gender=gender) + .values( + nickname=nickname, + location=location, + birthday=date.fromisoformat(birthday), + gender=gender, + ) ) await session.commit() await session.close() @@ -64,9 +104,7 @@ async def update_profile_basic(id: str, nickname: str, location: str, birthday: async def update_profile_sign(id: str, sign: str): session = async_session() await session.execute( - update(UserProfile) - .where(UserProfile.user_id == id) - .values(sign=sign) + update(UserProfile).where(UserProfile.user_id == id).values(sign=sign) ) await session.commit() await session.close() @@ -75,34 +113,49 @@ async def update_profile_sign(id: str, sign: str): async def update_profile_avatar(id: str, avatar_name: str): session = async_session() await session.execute( - update(UserProfile) - .where(UserProfile.user_id == id) - .values(avatar=avatar_name) + update(UserProfile).where(UserProfile.user_id == id).values(avatar=avatar_name) ) await session.commit() await session.close() -async def update_account(account: Literal['username', 'email', 'password'], id: str, value: str): +async def select_account_profile( + condition: Literal["username", "email"], + value: str, +) -> Tuple[bool, Union[Tuple[UserAccount, UserProfile], None]]: session = async_session() - match account: - case 'username': - await session.execute( - update(UserAccount) - .where(UserAccount.id == id) - .values(username=value) + res: ScalarResult[Tuple[UserAccount, UserProfile]] + match condition: + case "username": + # must use join otherwise the result will be Cartesian product + res = await session.execute( + select(UserAccount, UserProfile) + .join(UserAccount.profile) + .where(UserAccount.username == value) ) - case 'email': - await session.execute( - update(UserAccount) - .where(UserAccount.id == id) - .values(email=value) + case "email": + res = await session.execute( + select(UserAccount, UserProfile) + .join(UserAccount.profile) + .where(UserAccount.email == value) ) - case 'password': - await session.execute( - update(UserAccount) - .where(UserAccount.id == id) - .values(password=value) - ) - await session.commit() + await session.close() + + try: + return True, res.one() + except NoResultFound: + return False, None + + +async def select_multiuser_info( + applicant_ids: list, +) -> Sequence[Row[Tuple[UserAccount, UserProfile]]]: + session = async_session() + res: Result = await session.execute( + select(UserAccount, UserProfile) + .join(UserAccount.profile) + .where(UserAccount.id.in_(applicant_ids)) + ) + await session.close() + return res.all() diff --git a/src/database/json_typeddict.py b/src/database/json_typeddict.py new file mode 100644 index 0000000..e323599 --- /dev/null +++ b/src/database/json_typeddict.py @@ -0,0 +1,11 @@ +from typing import TypedDict + + +class FriendSetting(TypedDict): + friendRemark: str | None + friendGroup: str + + +class GroupChatSetting(TypedDict): + groupChatRemark: str | None + myRemark: str | None diff --git a/src/database/models.py b/src/database/models.py index 8e0dd88..ee91096 100755 --- a/src/database/models.py +++ b/src/database/models.py @@ -4,9 +4,11 @@ from enum import StrEnum from sqlalchemy.dialects.postgresql import JSONB from sqlalchemy.orm import DeclarativeBase, relationship from sqlalchemy.orm import mapped_column, Mapped -from sqlalchemy import String, Integer, Enum, DateTime, ARRAY, Date +from sqlalchemy import String, Integer, Enum, ARRAY, Date from sqlalchemy import ForeignKey +from .json_typeddict import * + class Base(DeclarativeBase): pass @@ -20,28 +22,30 @@ class UserAccount(Base): email: Mapped[str] = mapped_column(String, unique=True) password: Mapped[str] = mapped_column(String(60)) updated_at: Mapped[datetime] = mapped_column(default=datetime.now) - created_at: Mapped[datetime] = mapped_column(default=datetime.now, onupdate=datetime.now) - profile: Mapped['UserProfile'] = relationship(back_populates='user') - contact: Mapped['Contact'] = relationship(back_populates='user') + created_at: Mapped[datetime] = mapped_column( + default=datetime.now, onupdate=datetime.now + ) + profile: Mapped["UserProfile"] = relationship(back_populates="user") + contact: Mapped["Contact"] = relationship(back_populates="user") def __repr__(self): - return f'UserAccount(username={self.username}, email={self.email})' + return f"UserAccount(username={self.username}, email={self.email})" def to_dict(self) -> dict: return { - 'id': self.id, - 'username': self.username, - 'email': self.email, + "id": self.id, + "username": self.username, + "email": self.email, } class Gender(StrEnum): - man = 'man' - woman = 'woman' + man = "man" + woman = "woman" class UserProfile(Base): - __tablename__ = 'user_profile' + __tablename__ = "user_profile" id: Mapped[int] = mapped_column(Integer, primary_key=True) nickname: Mapped[str] = mapped_column(String) @@ -51,31 +55,72 @@ class UserProfile(Base): status: Mapped[str] = mapped_column(String, nullable=True) sign: Mapped[str] = mapped_column(String, nullable=True) avatar: Mapped[str] = mapped_column(String, nullable=True) - user_id: Mapped[str] = mapped_column(ForeignKey('user_account.id', ondelete='CASCADE')) - user: Mapped['UserAccount'] = relationship(back_populates='profile') + user_id: Mapped[str] = mapped_column( + ForeignKey("user_account.id", ondelete="CASCADE") + ) + user: Mapped["UserAccount"] = relationship(back_populates="profile") def __repr__(self): - return f'UserProfile(user={self.user_id},' \ - f'nickname={self.nickname})' + return f"UserProfile(user={self.user_id}," f"nickname={self.nickname})" - def to_dict_all(self): + def to_dict(self): return { - 'nickname': self.nickname, - 'gender': self.gender, - 'birthday': self.birthday.isoformat(), - 'location': self.location, - 'status': self.status, - 'sign': self.sign, - 'avatar': self.avatar, + "nickname": self.nickname, + "gender": self.gender, + "birthday": self.birthday and self.birthday.isoformat(), + "location": self.location, + "status": self.status, + "sign": self.sign, + "avatar": self.avatar, } class Contact(Base): - __tablename__ = 'contact' + __tablename__ = "contact" id: Mapped[int] = mapped_column(Integer, primary_key=True) - friends: Mapped[dict] = mapped_column(JSONB, nullable=True) + friends: Mapped[dict[str, FriendSetting]] = mapped_column(JSONB, nullable=True) friend_groups: Mapped[list[str]] = mapped_column(ARRAY(String), nullable=True) - group_chats: Mapped[dict] = mapped_column(JSONB, nullable=True) - user_id: Mapped[str] = mapped_column(ForeignKey('user_account.id', ondelete='CASCADE')) - user: Mapped['UserAccount'] = relationship(back_populates='contact') \ No newline at end of file + group_chats: Mapped[dict[str, GroupChatSetting]] = mapped_column( + JSONB, nullable=True + ) + user_id: Mapped[str] = mapped_column( + ForeignKey("user_account.id", ondelete="CASCADE") + ) + user: Mapped["UserAccount"] = relationship(back_populates="contact") + + def __repr__(self): + return ( + f"Contact(" + f"user={self.user_id}, " + f"friends={self.friends}, " + f"friend_group={self.friend_groups}, " + f"group_chats={self.group_chats}" + f")" + ) + + +class Apply(Base): + __tablename__ = "apply" + + id: Mapped[int] = mapped_column(Integer, primary_key=True) + relation: Mapped[int] = mapped_column(Integer) + applicant: Mapped[str] = mapped_column(String(26)) + recipient: Mapped[str] = mapped_column(String(26)) + group_chat_id: Mapped[str] = mapped_column(String(26), nullable=True) + hello: Mapped[str] = mapped_column(String) + setting: Mapped[dict] = mapped_column(JSONB) + created_at: Mapped[datetime] = mapped_column( + default=datetime.now, onupdate=datetime.now + ) + + def to_dict(self): + return { + "relation": self.relation, + "applicant": self.applicant, + "recipient": self.recipient, + "groupChatId": self.group_chat_id, + "hello": self.hello, + "setting": self.setting, + "createdAt": self.created_at.strftime("%y-%m-%d %H:%M:%S"), + } diff --git a/src/main.py b/src/main.py index d5c8fdb..a0704b9 100755 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,5 @@ from fastapi import FastAPI, Depends +from starlette.responses import FileResponse from .dependencies import verify_token from .utils.email_code import smtp @@ -6,21 +7,33 @@ from .routers.signin import router as signin_router from .routers.signup import router as signup_router from .routers.user_profile import router as user_profile_router from .routers.user_account import router as user_account_router +from .routers.search import router as search_router +from .routers.apply import router as apply_router +from .utils.static_file import create_zip_file app = FastAPI() app.include_router(signup_router) app.include_router(signin_router) app.include_router(user_profile_router, dependencies=[Depends(verify_token)]) app.include_router(user_account_router, dependencies=[Depends(verify_token)]) +app.include_router(search_router, dependencies=[Depends(verify_token)]) +app.include_router(apply_router, dependencies=[Depends(verify_token)]) -@app.on_event('shutdown') +@app.on_event("shutdown") def close_smtp(): smtp.close() -@app.get('/') +@app.get("/") async def main(): - return {'code': 10000, 'msg': 'hello world'} + return {"code": 10000, "msg": "hello world"} + +@app.get("/zipfile") +async def get_zipfile(): + file = create_zip_file( + ["luhptjjk1688921163.png", "pnjdvldw1688921358.png"], "avatars" + ) + return FileResponse(file) diff --git a/src/response_models/search_response.py b/src/response_models/search_response.py new file mode 100644 index 0000000..5325916 --- /dev/null +++ b/src/response_models/search_response.py @@ -0,0 +1,5 @@ +from typing import Optional + +from pydantic import BaseModel + +from .base import BaseResponseModel diff --git a/src/response_models/user_response.py b/src/response_models/user_response.py index dc4bd33..6f0ab9f 100755 --- a/src/response_models/user_response.py +++ b/src/response_models/user_response.py @@ -38,5 +38,22 @@ class UserProfileResponse(BaseResponseModel): data: _UserProfile +class _UserAccountProfile(BaseModel): + id: str + username: str + email: str + nickname: str + gender: str | None + birthday: str | None + location: str | None + status: str | None + sign: str | None + avatar: str | None + + +class UserAccountProfileResponse(BaseResponseModel): + data: _UserAccountProfile + + class UserAvatarResponse(BaseResponseModel): data: str diff --git a/src/routers/apply.py b/src/routers/apply.py new file mode 100644 index 0000000..c4896df --- /dev/null +++ b/src/routers/apply.py @@ -0,0 +1,93 @@ +from fastapi import APIRouter, Query +from fastapi.responses import FileResponse +from pydantic import BaseModel + +from src.crud import apply_crud, user_crud, multitable_crud +from ..response_models.base import BaseResponseModel +from ..utils.static_file import create_zip_file + +router = APIRouter(prefix="/apply", tags=["apply"]) + + +class ApplyInfo(BaseModel): + relation: int + applicant: str + recipient: str + group_chat_id: str | None = None + hello: str + setting: dict[str, str] = {} + + +class AcceptInfo(BaseModel): + relation: int + applicant: str + recipient: str + group_chat_id: str | None = None + applicant_setting: dict + recipient_setting: dict + + +class RefuseInfo(BaseModel): + relation: int + applicant: str + recipient: str + + +@router.post("/friend", response_model=BaseResponseModel) +async def apply_friend(apply_info: ApplyInfo): + await apply_crud.insert_apply(**apply_info.model_dump()) + return {"code": 10600, "msg": "Apply Friend Successfully"} + + +@router.get("/list") +async def get_apply_list(recipient: str): + res = await apply_crud.select_apply_all(recipient) + if not res: + return {"code": 10601, "msg": "The Apply List is Empty"} + else: + data = [] + for apply in res: + data.append(apply.to_dict()) + print(data) + return { + "code": 10600, + "msg": "Get All Apply List Successfully", + "data": data, + } + + +@router.get("/applicant_profiles") +async def get_applicant_profiles(applicant_ids: list[str] = Query(default=None)): + res = await user_crud.select_multiuser_info(applicant_ids) + applicant_profiles = {} + for applicant in res: + applicant_profiles[applicant[0].id] = applicant[0].to_dict() + applicant_profiles[applicant[0].id].update(applicant[1].to_dict()) + print(applicant_profiles) + return { + "code": 10600, + "msg": "Get Applicant Information Successfully", + "data": applicant_profiles, + } + + +@router.get("/applicant_avatars") +async def download_applicant_avatars(avatars: list[str] = Query(default=None)): + file_path = create_zip_file(avatars, "avatars") + return FileResponse(file_path) + + +@router.post("/accept") +async def accept_apply(accept_info: AcceptInfo): + try: + await multitable_crud.insert_contact_friend(**accept_info.model_dump()) + return {"code": 10600, "msg": "Add Friend Successfully"} + except Exception as e: + print(f"接受添加好友请求出错....: {e}") + return {"code": 10601, "msg": "Something Went Wrong On the Server"} + + +@router.post("/refuse") +async def refuse_apply(refuse_info: RefuseInfo): + await apply_crud.delete_apply(**refuse_info.model_dump()) + return {"code": 10600, "msg": "Refuse Apply Successfully"} diff --git a/src/routers/search.py b/src/routers/search.py new file mode 100644 index 0000000..78bdd9d --- /dev/null +++ b/src/routers/search.py @@ -0,0 +1,22 @@ +from typing import Literal + +from fastapi import APIRouter + +from ..crud import user_crud +from ..response_models.user_response import UserAccountProfileResponse + +router = APIRouter(prefix="/search", tags=["search"]) + + +@router.get("/friend", response_model=UserAccountProfileResponse) +async def search_contact_by(condition: Literal["username", "email"], value: str): + is_existence, res = await user_crud.select_account_profile(condition, value) + if not is_existence: + return {"code": 10501, "msg": "The User Does Not Exist"} + + data = {} + + data.update(res[0].to_dict()) + data.update(res[1].to_dict()) + + return {"code": 10500, "msg": "Search Successfully", "data": data} diff --git a/src/routers/signin.py b/src/routers/signin.py index fa8f214..a51bbf2 100755 --- a/src/routers/signin.py +++ b/src/routers/signin.py @@ -5,7 +5,7 @@ from pydantic import BaseModel from jose import ExpiredSignatureError, JWTError -from ..crud.user_crud import select_user_by +from ..crud.user_crud import select_account_by from ..utils.password import verify_password from ..utils.token_handler import create_signin_token, oauth2_scheme, verify_signin_token from ..response_models.user_response import UserAccountResponse, TokenCreationResponse, TokenSigninResponse @@ -22,7 +22,7 @@ class TokenPayload(BaseModel): async def signin_by_username(form_data: OAuth2PasswordRequestForm = Depends()): username = form_data.username password = form_data.password - is_existence, user = await select_user_by('username', username) + is_existence, user = await select_account_by('username', username) if not is_existence: return {'code': 10201, 'msg': 'Username or Password Is Incorrect'} @@ -45,7 +45,7 @@ async def create_token(token_payload: TokenPayload): async def signin_by_token(token: str = Depends(oauth2_scheme)): try: new_token, id = verify_signin_token(token) - _, user = await select_user_by('id', id) + _, user = await select_account_by('id', id) if new_token: return {'code': 10200, 'msg': 'Sign in Successfully', 'data': user.to_dict(), 'token': new_token} else: diff --git a/src/routers/signup.py b/src/routers/signup.py index 32840e4..7d54447 100755 --- a/src/routers/signup.py +++ b/src/routers/signup.py @@ -19,7 +19,7 @@ class SignUpAccount(BaseModel): @router.get('/has_existed') async def has_account_existed(condition: Literal['username', 'email'], value: str): - (res, _) = await user_crud.select_user_by(condition, value) + (res, _) = await user_crud.select_account_by(condition, value) if res: return {'code': 10101, 'msg': f'{condition.capitalize()} Has Existed'} else: diff --git a/src/routers/user_account.py b/src/routers/user_account.py index ac10c66..fe92d3d 100644 --- a/src/routers/user_account.py +++ b/src/routers/user_account.py @@ -4,6 +4,7 @@ from pydantic import BaseModel from ..crud import user_crud from ..utils import password from ..utils.email_code import send_email, has_code, verify_code +from ..response_models.base import BaseResponseModel router = APIRouter(prefix='/user_account', tags=['user_account']) @@ -16,9 +17,9 @@ class ChangedAccount(BaseModel): code: str | None = None -@router.post('/change/username') +@router.post('/change/username', response_model=BaseResponseModel) async def change_username(changed_account: ChangedAccount): - is_existed, user = await user_crud.select_user_by('username', changed_account.username) + is_existed, user = await user_crud.select_account_by('username', changed_account.username) if is_existed: return {'code': 10401, 'msg': f'This Username ({changed_account.username}) Has Been Used'} @@ -27,9 +28,9 @@ async def change_username(changed_account: ChangedAccount): return {'code': 10400, 'msg': 'Update Username Successfully'} -@router.get('/get/email_code') +@router.get('/get/email_code', response_model=BaseResponseModel) async def get_change_email_code(email: str, background_tasks: BackgroundTasks): - is_existed, _ = await user_crud.select_user_by('email', email) + is_existed, _ = await user_crud.select_account_by('email', email) if is_existed: return {'code': 10401, 'msg': f'This Email ({email}) Has Been Used'} @@ -40,7 +41,7 @@ async def get_change_email_code(email: str, background_tasks: BackgroundTasks): return {'code': 10400, 'msg': 'Send Verification Code Successfully'} -@router.post('/change/email') +@router.post('/change/email', response_model=BaseResponseModel) async def change_email(changed_account: ChangedAccount): is_correct = verify_code(changed_account.email, changed_account.code) if not is_correct: @@ -51,7 +52,7 @@ async def change_email(changed_account: ChangedAccount): return {'code': 10400, 'msg': 'Update Email Successfully'} -@router.get('/get/password_code') +@router.get('/get/password_code', response_model=BaseResponseModel) async def get_change_password_code(email: str, background_tasks: BackgroundTasks): if has_code(email): return {'code': 10402, 'msg': f'Code of Email ({email}) Is Still Available'} @@ -60,7 +61,7 @@ async def get_change_password_code(email: str, background_tasks: BackgroundTasks return {'code': 10400, 'msg': 'Send Verification Code Successfully'} -@router.post('/change/password') +@router.post('/change/password', response_model=BaseResponseModel) async def change_password(changed_account: ChangedAccount): is_correct = verify_code(changed_account.email, changed_account.code) if not is_correct: diff --git a/src/routers/user_profile.py b/src/routers/user_profile.py index 47e43d2..4f508ec 100755 --- a/src/routers/user_profile.py +++ b/src/routers/user_profile.py @@ -8,11 +8,15 @@ from fastapi.encoders import jsonable_encoder from anyio import open_file from ..crud import user_crud -from ..response_models.user_response import UserProfileResponse, UserAvatarResponse +from ..response_models.user_response import ( + BaseResponseModel, + UserProfileResponse, + UserAvatarResponse, +) from ..utils import static_file -router = APIRouter(prefix='/user_profile', tags=['user_profile']) +router = APIRouter(prefix="/user_profile", tags=["user_profile"]) class Uint8List(BaseModel): @@ -28,39 +32,43 @@ class ChangedProfile(BaseModel): sign: Optional[str] = None -@router.get('/my', response_model=UserProfileResponse) +@router.get("/my", response_model=UserProfileResponse) async def get_profile(id: str): profile = await user_crud.select_profile(id) return JSONResponse( content=jsonable_encoder( - {'code': 10300, 'msg': 'Get My Profile Successfully', 'data': profile.to_dict_all()}, + { + "code": 10300, + "msg": "Get My Profile Successfully", + "data": profile.to_dict(), + }, ) ) -@router.get('/avatar') +@router.get("/avatar") async def download_avatar(avatar_filename: str): - avatar_dir_path = static_file.create_avatar_dir() + avatar_dir_path = static_file.create_dir("avatars") return FileResponse(avatar_dir_path / avatar_filename) -@router.post('/change/avatar', response_model=UserAvatarResponse) +@router.post("/change/avatar", response_model=UserAvatarResponse) async def change_avatar(id: str, file: Uint8List): - avatar_dir_path = static_file.create_avatar_dir() + avatar_dir_path = static_file.create_dir("avatars") avatar_filename = static_file.create_avatar_filename() - async with await open_file(avatar_dir_path / avatar_filename, 'wb') as f: + async with await open_file(avatar_dir_path / avatar_filename, "wb") as f: await f.write(bytearray(file.file)) await user_crud.update_profile_avatar(id, avatar_filename) - return {'code': 10300, 'msg': 'Update Avatar Successfully', 'data': avatar_filename} + return {"code": 10300, "msg": "Update Avatar Successfully", "data": avatar_filename} -@router.post('/change/{aspect}') +@router.post("/change/{aspect}", response_model=BaseResponseModel) async def change_profile(aspect: str, changed_profile: ChangedProfile): match aspect: - case 'basic': + case "basic": await user_crud.update_profile_basic( changed_profile.id, changed_profile.nickname, @@ -68,11 +76,10 @@ async def change_profile(aspect: str, changed_profile: ChangedProfile): changed_profile.birthday, changed_profile.gender, ) - case 'sign': + case "sign": await user_crud.update_profile_sign( - changed_profile.id, - changed_profile.sign + changed_profile.id, changed_profile.sign ) case _: - return {'code': 10301, 'msg': f'No /change/{aspect} Path'} - return {'code': 10300, 'msg': f'Update {aspect} Profile Successfully'} \ No newline at end of file + return {"code": 10301, "msg": f"No /change/{aspect} Path"} + return {"code": 10300, "msg": f"Update {aspect} Profile Successfully"} diff --git a/src/utils/static_file.py b/src/utils/static_file.py index 8af219d..1a4f68e 100755 --- a/src/utils/static_file.py +++ b/src/utils/static_file.py @@ -1,15 +1,42 @@ import os import random +from typing import Literal from pathlib import Path from datetime import datetime +from zipfile import ZipFile -alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', - 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', - ] +alphabet = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", +] -def create_avatar_dir() -> Path: - avatar_dir_path = Path(os.getcwd()) / 'static' / 'avatars' +def create_dir(dir_name: str) -> Path: + avatar_dir_path = Path(os.getcwd()) / "static" / dir_name if not avatar_dir_path.exists(): avatar_dir_path.mkdir() return avatar_dir_path @@ -17,5 +44,19 @@ def create_avatar_dir() -> Path: def create_avatar_filename() -> str: timestamp = int(datetime.now().timestamp()) - random_prefix = ''.join(random.choices(alphabet, k=8)) - return f'{random_prefix}{timestamp}.png' + random_prefix = "".join(random.choices(alphabet, k=8)) + return f"{random_prefix}{timestamp}.png" + + +def create_zip_file(filenames: list[str], file_type: Literal["avatars"]) -> Path: + zip_filename = f"temp_{file_type}_{round(datetime.now().timestamp())}.zip" + zip_dir = Path(os.getcwd()) / "static" / "temp" + file_dir = Path(os.getcwd()) / "static" / file_type + if not zip_dir.exists(): + zip_dir.mkdir(parents=True) + + with ZipFile(zip_dir / zip_filename, "w") as zip_file: + for filename in filenames: + zip_file.write(file_dir / filename, arcname=filename) + + return zip_dir / zip_filename