diff --git a/.gitignore b/.gitignore index 8965ddb..452a5f4 100644 --- a/.gitignore +++ b/.gitignore @@ -124,6 +124,9 @@ celerybeat.pid # SageMath parsed files *.sage.py +# Image Folder +/images/ + # Environments .venv env/ diff --git a/api/app.py b/api/app.py index a8cc29f..2411075 100644 --- a/api/app.py +++ b/api/app.py @@ -9,6 +9,7 @@ user_rating_history, user_stats, user_token, + user_images, users, ) @@ -19,6 +20,7 @@ app.include_router(user_rating_history.router) app.include_router(user_stats.router) app.include_router(user_token.router) +app.include_router(user_images.router) app.include_router(users.router) diff --git a/api/database/functions.py b/api/database/functions.py index fb19e32..550ce41 100644 --- a/api/database/functions.py +++ b/api/database/functions.py @@ -2,6 +2,8 @@ import logging import random import re +import string +import sys import traceback from asyncio.tasks import create_task from collections import namedtuple @@ -9,14 +11,13 @@ from typing import List from api.database.database import USERDATA_ENGINE, Engine, EngineType +from api.database.models import Users, UserToken from fastapi import HTTPException from sqlalchemy import Text, text from sqlalchemy.exc import InternalError, OperationalError from sqlalchemy.ext.asyncio import AsyncResult, AsyncSession from sqlalchemy.sql.expression import insert, select -from api.database.models import UserToken, Users - logger = logging.getLogger(__name__) @@ -175,8 +176,17 @@ async def batch_function(function, data, batch_size=100): batches = [] for i in range(0, len(data), batch_size): logger.debug({"batch": {f"{function.__name__}": f"{i}/{len(data)}"}}) - batch = data[i: i + batch_size] + batch = data[i : i + batch_size] batches.append(batch) await asyncio.gather(*[create_task(function(batch)) for batch in batches]) return + + +async def image_token_generator(length=10): + return "".join( + random.SystemRandom().choice( + string.ascii_uppercase + string.ascii_lowercase + string.digits + ) + for _ in range(length) + ) diff --git a/api/database/models.py b/api/database/models.py index 5d899e6..a6a4157 100644 --- a/api/database/models.py +++ b/api/database/models.py @@ -1,4 +1,5 @@ from datetime import datetime +from enum import unique from numpy import integer from sqlalchemy import ( @@ -68,7 +69,7 @@ class UserImages(Base): bio = Column(TINYINT) banner = Column(TINYINT) image_key = Column(TINYTEXT) - image = Column(BLOB) + image = Column(TEXT, unique=True) size = Column(TINYINT) timestamp = Column(TIMESTAMP) @@ -142,7 +143,9 @@ class TrainerIdentificationInformation(Base): user_id = Column( ForeignKey("users.user_id", ondelete="RESTRICT", onupdate="RESTRICT") ) - content = Column(BLOB) + image = Column( + ForeignKey("user_images.image", ondelete="RESTRICT", onupdate="RESTRICT") + ) content_type = Column(TINYINT) timestamp = Column(TIMESTAMP) diff --git a/api/routers/trainer_identification_information.py b/api/routers/trainer_identification_information.py index 758742f..e69de29 100644 --- a/api/routers/trainer_identification_information.py +++ b/api/routers/trainer_identification_information.py @@ -1,74 +0,0 @@ -# from datetime import datetime -# import json -# from typing import Optional -# from urllib.request import Request - -# from api.database.functions import USERDATA_ENGINE, EngineType, sqlalchemy_result, verify_token -# from api.database.models import TrainerIdentificationInformation -# from fastapi import APIRouter, HTTPException, Query, status -# from pydantic import BaseModel -# from pydantic.fields import Field -# from pymysql import Timestamp -# from sqlalchemy import DATETIME, TIMESTAMP, func, select -# from sqlalchemy.dialects.mysql import Insert -# from sqlalchemy.ext.asyncio import AsyncSession -# from sqlalchemy.orm import aliased -# from sqlalchemy.sql.expression import Select, select, insert - -# router = APIRouter() - - -# class trainer_identification_information(BaseModel): -# """ -# trainer_identification_information base model containing the types and content expected by the database -# """ - - -# @router.get( -# "/V1/trainer-identification-information/", -# tags=["trainer", "trainer identification information"], -# ) -# async def get_trainer_identification_information( -# token: str, -# ID: Optional[int], -# user_id: int, -# content_type: Optional[int], -# timestamp=Optional[datetime], -# # content = Column(BLOB), -# row_count: Optional[int] = Query(100, ge=1, le=1000), -# page: Optional[int] = Query(1, ge=1), -# ) -> json: - -# table = TrainerIdentificationInformation -# sql: Select = select(table) - -# sql = sql.limit(row_count).offset(row_count * (page - 1)) - -# async with USERDATA_ENGINE.get_session() as session: -# session: AsyncSession = session -# async with session.begin(): -# data = await session.execute(sql) - -# data = sqlalchemy_result(data) -# return data.rows2dict() - - -# @router.post( -# "/V1/trainer-identification-information", -# tags=["trainer", "trainer identification information"], -# ) -# async def post_trainer_identification_status( -# trainer_identification_information: trainer_identification_information, -# ) -> json: - -# values = trainer_identification_information.dict() -# table = TrainerIdentificationInformation -# sql = insert(table).values(values) -# sql = sql.prefix_with("ignore") - -# async with USERDATA_ENGINE.get_session() as session: -# session: AsyncSession = session -# async with session.begin(): -# data = await session.execute(sql) - -# return {"ok": "ok"} diff --git a/api/routers/user_images.py b/api/routers/user_images.py index e69de29..d1c3b34 100644 --- a/api/routers/user_images.py +++ b/api/routers/user_images.py @@ -0,0 +1,217 @@ +from genericpath import exists +import json +import os +import shutil +from datetime import datetime +from pickletools import optimize +from typing import Optional +from urllib.request import Request + +from api.database.functions import ( + USERDATA_ENGINE, + EngineType, + image_token_generator, + sqlalchemy_result, + verify_token, +) +from api.database.models import UserImages +from fastapi import APIRouter, File, HTTPException, Query, UploadFile, status +from pydantic import BaseModel +from pydantic.fields import Field +from pymysql import Timestamp +from pyparsing import Opt +from sqlalchemy import BLOB, DATETIME, TIMESTAMP, func, select +from sqlalchemy.dialects.mysql import Insert +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import aliased +from sqlalchemy.sql.expression import Select, insert, select + +from api.routers.users import get_users +from enum import Enum + +router = APIRouter() + + +class image_category(str, Enum): + bio = "bio" + profile = "profile" + banner = "banner" + chat = "chat" + license_front = "license_front" + license_back = "license_back" + + +@router.get( + "/V1/user-images/", + tags=["user", "images"], +) +async def get_user_images( + token: str, + login: str, + s_user_id: Optional[int] = None, + chat: Optional[bool] = False, + profile: Optional[bool] = False, + bio: Optional[bool] = False, + banner: Optional[bool] = False, + image_key: Optional[str] = None, + size: Optional[int] = None, + timestamp: Optional[datetime] = None, + ID: Optional[int] = None, + image: UploadFile = File(None), + row_count: Optional[int] = Query(100, ge=1, le=1000), + page: Optional[int] = Query(1, ge=1), +) -> json: + """ + Args:\n + token (str): user token information\n + login (str): user login information\n + s_user_id (Optional[int], optional): sending user id. Defaults to None.\n + chat (Optional[bool], optional): True if the content is chat. Defaults to False.\n + profile (Optional[bool], optional): True if the content is a profile. Defaults to False.\n + bio (Optional[bool], optional): True if the content is in a bio. Defaults to False.\n + banner (Optional[bool], optional): True if the content is a banner image. Defaults to False.\n + image_key (Optional[str], optional): The image key for the image. Defaults to None.\n + size (Optional[int], optional): The size of the image in KB. Defaults to None.\n + timestamp (Optional[datetime], optional): timestamp of image creation. Defaults to None.\n + ID (Optional[int], optional): ID of the image, increments automatically. Defaults to None.\n + image (UploadFile, optional): Image path on server itself. Defaults to File(None).\n + row_count (Optional[int], optional): Rows to pull for relevant data. Defaults to Query(100, ge=1, le=1000).\n + page (Optional[int], optional): Pages to pull. Defaults to Query(1, ge=1).\n + Returns:\n + json: _description_\n + """ + + if not await verify_token(login=login, token=token, access_level=9): + return + + table = UserImages + sql: Select = select(table) + + if s_user_id is not None: + sql = sql.where(table.s_user_id == s_user_id) + + if chat is not None: + sql = sql.where(table.chat == chat) + + if profile is not None: + sql = sql.where(table.profile == profile) + + if bio is not None: + sql = sql.where(table.bio == bio) + + if banner is not None: + sql = sql.where(table.banner == banner) + + if image_key is not None: + sql = sql.where(table.image_key == image_key) + + if size is not None: + sql = sql.where(table.size == size) + + if timestamp is not None: + sql = sql.where(table.timestamp == timestamp) + + if ID is not None: + sql = sql.where(table.ID == ID) + + if image is not None: + sql = sql.where(table.image == image) + + sql = sql.limit(row_count).offset(row_count * (page - 1)) + + async with USERDATA_ENGINE.get_session() as session: + session: AsyncSession = session + async with session.begin(): + data = await session.execute(sql) + + data = sqlalchemy_result(data) + return data.rows2dict() + + +@router.post( + "/V1/user-images", + tags=["user", "images"], +) +async def post_user_images( + login: str, + token: str, + selection: image_category, + image: UploadFile = File(...), +) -> json: + """ + Args:\n + login (str): Login informatoin\n + token (str): Token informatoin\n + selection (image_category): Image category selection for categorization.\n + image (UploadFile, optional): Uploaded image. Defaults to File(...).\n + + Returns:\n + json: {'ok':'ok'}\n + """ + + # verify user auth level + if not await verify_token(login=login, token=token, access_level=9): + return + + # get user ID + user_data = await get_users(login=login, token=token, self_lookup=True) + user_id = user_data[0]["user_id"] + + # generate image key + image_key = await image_token_generator(length=50) + + # set directory + directory = f"images/{user_id}" + image_path = f"{directory}/{image_key}" + + # create directory + try: + os.mkdir(directory) + except FileExistsError: + pass + + # store image in directory + with open(image_path, "wb") as image_buffer: + shutil.copyfileobj(image.file, image_buffer) + + # image size in KB + size = os.path.getsize(image_path) / 1000 + + # set up selection name and relevant values for + selection_choice = selection.name + bio = profile = banner = chat = license_front = license_back = 0 + + if selection_choice == "bio": + bio = 1 + if selection_choice == "profile": + profile = 1 + if selection_choice == "banner": + banner = 1 + if selection_choice == "chat": + chat = 1 + if selection_choice == "license_front": + license_front = 1 + if selection_choice == "license_back": + license_back = 1 + + values = { + "s_user_id": user_id, + "chat": chat, + "profile": profile, + "bio": bio, + "banner": banner, + "image_key": image_key, + "image": image_path, + "size": size, + } + + table = UserImages + sql = insert(table).values(values) + sql = sql.prefix_with("ignore") + + async with USERDATA_ENGINE.get_session() as session: + session: AsyncSession = session + async with session.begin(): + data = await session.execute(sql) + + return {"ok": "ok"} diff --git a/api/routers/users.py b/api/routers/users.py index 448a4b6..7fca385 100644 --- a/api/routers/users.py +++ b/api/routers/users.py @@ -48,8 +48,9 @@ async def get_users( login: Optional[str] = None, password: Optional[str] = None, timestamp: Optional[datetime] = None, - row_count: Optional[int] = Query(100, ge=1, le=1000), - page: Optional[int] = Query(1, ge=1), + self_lookup: Optional[bool] = True, + row_count: Optional[int] = 100, + page: Optional[int] = 1, ) -> json: """ Args:\n @@ -58,6 +59,7 @@ async def get_users( login (Optional[str], optional): login username. Defaults to None.\n password (Optional[str], optional): hashed/salted password to be checked against on login. Defaults to None.\n timestamp (Optional[datetime], optional): timestamp of creation. Defaults to None.\n + self_lookup (Optional[bool]), optional: toggles if the get request is for a self-lookup row_count (Optional[int], optional): row count to be chosen at time of get. Defaults to Query(100, ge=1, le=1000).\n page (Optional[int], optional): page selection. Defaults to Query(1, ge=1).\n @@ -71,18 +73,16 @@ async def get_users( table = Users sql: Select = select(table) - if user_id is not None: - sql = sql.where(table.user_id == user_id) - + if not self_lookup: + if user_id is not None: + sql = sql.where(table.user_id == user_id) + if password is not None: + sql = sql.where(table.password == password) + if timestamp is not None: + sql = sql.where(table.timestamp == timestamp) if login is not None: sql = sql.where(table.login == login) - if password is not None: - sql = sql.where(table.password == password) - - if timestamp is not None: - sql = sql.where(table.timestamp == timestamp) - sql = sql.limit(row_count).offset(row_count * (page - 1)) async with USERDATA_ENGINE.get_session() as session: diff --git a/requirements.txt b/requirements.txt index ecd3b7c..d8bb49f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,18 +12,21 @@ charset-normalizer==2.0.11 click==8.0.3 colorama==0.4.4 cryptography==36.0.1 +discord-webhook==0.14.0 fastapi==0.73.0 greenlet==1.1.2 h11==0.13.0 idna==3.3 iniconfig==1.1.1 mypy-extensions==0.4.3 +mysql-connector-python==8.0.23 numpy==1.22.2 packaging==21.3 pandas==1.4.0 pathspec==0.9.0 platformdirs==2.4.1 pluggy==1.0.0 +protobuf==3.20.1 py==1.11.0 pycparser==2.21 pydantic==1.9.0 @@ -32,6 +35,7 @@ pyparsing==3.0.7 pytest==6.2.5 python-dateutil==2.8.2 python-dotenv==0.19.2 +python-multipart==0.0.5 pytz==2021.3 pytz-deprecation-shim==0.1.0.post0 requests==2.27.1 @@ -47,4 +51,4 @@ tzdata==2021.5 tzlocal==4.1 urllib3==1.26.8 uvicorn==0.17.3 -XlsxWriter==3.0.2 \ No newline at end of file +XlsxWriter==3.0.2