diff --git a/dockerfile b/dockerfile index 09f792b..fccd731 100644 --- a/dockerfile +++ b/dockerfile @@ -1,17 +1,9 @@ -# Используем официальный образ Python 3 FROM python:3.13-slim -# Устанавливаем рабочую директорию внутри контейнера WORKDIR /app -# Копируем requirements.txt в контейнер для установки зависимостей COPY requirements.txt ./ - -# Устанавливаем зависимости RUN pip install --no-cache-dir -r requirements.txt -# Копируем остальные файлы проекта в контейнер COPY . . - -# Определяем команду запуска приложения CMD ["python", "src/__main__.py"] diff --git a/src/api/feeds.py b/src/api/feeds.py index e69de29..37e91dd 100644 --- a/src/api/feeds.py +++ b/src/api/feeds.py @@ -0,0 +1,88 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from psycopg2._psycopg import connection + +import db.feeds as db +import settings.settings as settings +from api.models import Feed, Group, User +from api.utils import get_current_user +from db.feeds import get_groupname_by_feed_id +from db.groups import get_group +from db.internal import get_db_connection +from db.memberships import check_membership_exists + +feeds_router = APIRouter(prefix="/api/feeds", tags=["feeds"]) + + +@feeds_router.post("/feed") +async def read_feed( + feed_id: int, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + feed = Feed() + feed_data = db.get_feed(conn, feed_id) + if feed_data is None: + return HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such feed", + ) + feed.fill(feed_data) + + if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not allowed", + ) + + return feed + + +@feeds_router.post("/add") +async def add_feed( + feed_id: int, + picture_id: int, + value: int, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + groupname = get_groupname_by_feed_id(conn, feed_id) + if groupname is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such feed or feed is not linked to group", + ) + + group = Group() + group_data = get_group(conn, groupname) + if group_data is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such feed or feed is not linked to group", + ) + group.fill(group_data) + + if value == 0 and group.allow_skips is False: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Skips are disallowed", + ) + return db.create_feed(conn, current_user.username, feed_id, picture_id, value) + + +@feeds_router.post("/delete") +async def delete_feed( + username: str, + feed_id: int, + picture_id: int, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.role in settings.settings.admin_roles: + return db.delete_feed(conn, username, feed_id, picture_id) + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not allowed", + ) diff --git a/src/api/general.py b/src/api/general.py index f154c29..a1ddcec 100644 --- a/src/api/general.py +++ b/src/api/general.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, status import settings.settings as settings from api.models import User from api.utils import get_current_user +from settings.consts import API_EDITABLE_SETTINGS_LIST from settings.settings import load_settings, reset_settings, save_settings general_router = APIRouter(prefix="/api", tags=["general"]) @@ -25,7 +26,7 @@ async def get_settings(current_user: Annotated[User, Depends(get_current_user)]) return settings.settings -@general_router.post("/settings/update") +@general_router.post("/settings/update", description=API_EDITABLE_SETTINGS_LIST) async def update_settings(data: dict, current_user: Annotated[User, Depends(get_current_user)]): if current_user.role not in settings.settings.admin_roles: raise HTTPException( diff --git a/src/api/groups.py b/src/api/groups.py index 3e96d6d..89fcc1f 100644 --- a/src/api/groups.py +++ b/src/api/groups.py @@ -56,6 +56,19 @@ async def read_group_invite_code( ) return invite_code +@groups_router.post("/last_feed") +async def read_group_last_feed_id( + groupname: str, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not allowed", + ) + return db.get_group_last_feed_id(conn, groupname) + @groups_router.post("/add") async def add_group( diff --git a/src/api/models.py b/src/api/models.py index 5b9e028..37be80e 100644 --- a/src/api/models.py +++ b/src/api/models.py @@ -72,3 +72,29 @@ class Picture(BaseModel): url: str = "" metadata: dict | None = None created_at: datetime | None = None + + +class Swipe(BaseModel): + def fill(self, params): + self.username = params["username"] + self.feed_id = params["feed_id"] + self.picture_id = params["picture_id"] + self.value = params["value"] + self.created_at = params["created_at"] + username: str = "" + feed_id: int = -1 + picture_id: int = -1 + value: int = 0 + created_at: datetime | None = None + + +class Feed(BaseModel): + def fill(self, params): + self.id = params["id"] + self.groupname = params["groupname"] + self.image_ids = params["image_ids"] + self.created_at = params["created_at"] + id: int = -1 + groupname: str = "" + image_ids: list[int] = [] + created_at: datetime | None = None diff --git a/src/api/pictures.py b/src/api/pictures.py index 141bde4..40bbb97 100644 --- a/src/api/pictures.py +++ b/src/api/pictures.py @@ -62,12 +62,6 @@ async def add_picture( detail="Not allowed", ) - # if db.check_picture_existence(conn, groupname): - # raise HTTPException( - # status_code=status.HTTP_409_CONFLICT, - # detail="Picture already exists", - # ) - return { "id": db.create_picture(conn, source, external_id, url, metadata) } diff --git a/src/api/swipes.py b/src/api/swipes.py new file mode 100644 index 0000000..c31fcde --- /dev/null +++ b/src/api/swipes.py @@ -0,0 +1,147 @@ +from typing import Annotated + +from fastapi import APIRouter, Depends, HTTPException, status +from psycopg2._psycopg import connection + +import db.swipes as db +import settings.settings as settings +from api.models import Group, Swipe, User +from api.utils import get_current_user +from db.feeds import get_groupname_by_feed_id +from db.groups import get_group +from db.internal import get_db_connection +from db.memberships import check_membership_exists + +swipes_router = APIRouter(prefix="/api/swipes", tags=["swipes"]) + + +# Maybe endpoints should be remade +# to return id and then work with swipe id + +@swipes_router.post("/swipe") +async def read_swipe( + username: str, + feed_id: int, + picture_id: int, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + groupname = get_groupname_by_feed_id(conn, feed_id) + if groupname is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such feed or feed is not linked to group", + ) + if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not allowed", + ) + + swipe = Swipe() + swipe_data = db.get_swipe(conn, username, feed_id, picture_id) + if swipe_data is None: + return HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such swipe", + ) + swipe.fill(swipe_data) + return swipe + + +@swipes_router.post("/swipe/picture_id") +async def read_swipes_by_picture_id( + feed_id: int, + picture_id: int, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + groupname = get_groupname_by_feed_id(conn, feed_id) + if groupname is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such feed or feed is not linked to group", + ) + if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not allowed", + ) + return db.get_swipes_by_picture_id(conn, picture_id, feed_id) + + +@swipes_router.post("/swipe/user") +async def read_user_swipes( + username: str, + feed_id: int, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + groupname = get_groupname_by_feed_id(conn, feed_id) + if groupname is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such feed or feed is not linked to group", + ) + if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not allowed", + ) + return db.get_swipes_by_user(conn, username, feed_id) + + +@swipes_router.post("/add") +async def add_swipe( + feed_id: int, + picture_id: int, + value: int, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + groupname = get_groupname_by_feed_id(conn, feed_id) + if groupname is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such feed or feed is not linked to group", + ) + + group = Group() + group_data = get_group(conn, groupname) + if group_data is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="No such feed or feed is not linked to group", + ) + group.fill(group_data) + + # Check for trying to skip in + # a group with skips disabled + if value == 0 and group.allow_skips is False: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Skips are disallowed", + ) + + result = db.create_swipe(conn, current_user.username, feed_id, picture_id, value) + if result: + # TODO: call function to like picture on the platform + pass + return result + + +@swipes_router.post("/delete") +async def delete_swipe( + username: str, + feed_id: int, + picture_id: int, + conn: Annotated[connection, Depends(get_db_connection)], + current_user: Annotated[User, Depends(get_current_user)] +): + if current_user.role in settings.settings.admin_roles: + return db.delete_swipe(conn, username, feed_id, picture_id) + else: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Not allowed", + ) diff --git a/src/api/utils.py b/src/api/utils.py index 08dc892..b5362aa 100644 --- a/src/api/utils.py +++ b/src/api/utils.py @@ -6,8 +6,6 @@ import jwt from fastapi import Depends, HTTPException, status from fastapi.security import OAuth2PasswordBearer from jwt.exceptions import InvalidTokenError - -# from passlib.context import CryptContext from psycopg2._psycopg import connection import db.groups @@ -16,8 +14,6 @@ import settings.startup_settings as startup_settings from api.models import Group, TokenData, User from db.internal import get_db_connection -# pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") - oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @@ -96,6 +92,7 @@ async def get_current_user( return user + def get_group_by_name( conn: connection, groupname: str diff --git a/src/create_app.py b/src/create_app.py index 05439ec..b29c31a 100644 --- a/src/create_app.py +++ b/src/create_app.py @@ -9,6 +9,7 @@ from api.general import general_router from api.groups import groups_router from api.memberships import memberships_router from api.pictures import pictures_router +from api.swipes import swipes_router from api.users import users_router from db.internal import connect_db, disconnect_db from settings import startup_settings @@ -45,6 +46,7 @@ def create_app(): app.include_router(groups_router) app.include_router(memberships_router) app.include_router(pictures_router) + app.include_router(swipes_router) app.add_event_handler("shutdown", disconnect_db) app.add_event_handler("shutdown", settings_down) diff --git a/src/db/feeds.py b/src/db/feeds.py new file mode 100644 index 0000000..387b2fb --- /dev/null +++ b/src/db/feeds.py @@ -0,0 +1,80 @@ +import psycopg2.extras +from psycopg2._psycopg import connection + +# feed create and delete + +def create_feed( + conn: connection, + groupname: int, + image_ids: list[int] +): + with conn.cursor() as cur: + cur.execute( + """ + insert into picrinth.feeds + (groupname, image_ids, created_at) + values (%s, %s, %s, %s, now()) + returning id + """, + (groupname, image_ids), + ) + conn.commit() + return cur.rowcount > 0 + + +def delete_feed( + conn: connection, + feed_id: int +): + with conn.cursor() as cur: + cur.execute( + """ + delete from picrinth.feeds + where feed_id = %s + """, + (feed_id,), + ) + conn.commit() + return cur.rowcount > 0 + + +# feed receiving + +def get_feed( + conn: connection, + feed_id: int +): + with conn.cursor() as cur: + cur.execute( + """ + select groupname + from picrinth.feeds + where feed_id = %s + """, + (feed_id,), + ) + result = cur.fetchone() + if result is None: + return None + return cur.fetchone()[0] # type: ignore + + +# additional + +def get_groupname_by_feed_id( + conn: connection, + feed_id: int +): + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute( + """ + select groupname + from picrinth.feeds + where feed_id = %s + """, + (feed_id,), + ) + result = cur.fetchone() + if result is None: + return None + return cur.fetchone()[0] # type: ignore diff --git a/src/db/groups.py b/src/db/groups.py index 038597b..bbb7832 100644 --- a/src/db/groups.py +++ b/src/db/groups.py @@ -243,6 +243,7 @@ def get_groupname_by_invite_code( return None return result[0] # type: ignore + def get_group_invite_code( conn: connection, groupname: str @@ -260,3 +261,21 @@ def get_group_invite_code( if result is None: return None return result[0] # type: ignore + +def get_group_last_feed_id( + conn: connection, + groupname: str +): + with conn.cursor() as cur: + cur.execute( + """ + select last_feed_id + from picrinth.groups + where groupname = %s + """, + (groupname,), + ) + result = cur.fetchone() + if result is None: + return None + return result[0] # type: ignore diff --git a/src/db/memberships.py b/src/db/memberships.py index 1544495..16b12e8 100644 --- a/src/db/memberships.py +++ b/src/db/memberships.py @@ -1,4 +1,3 @@ -import psycopg2.extras from psycopg2._psycopg import connection # membership create and delete @@ -65,7 +64,7 @@ def get_memberships_by_username( conn: connection, username: str ): - with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + with conn.cursor() as cur: cur.execute( """ select * @@ -81,7 +80,7 @@ def get_memberships_by_groupname( conn: connection, groupname: str ): - with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + with conn.cursor() as cur: cur.execute( """ select * diff --git a/src/db/swipes.py b/src/db/swipes.py new file mode 100644 index 0000000..c456c9d --- /dev/null +++ b/src/db/swipes.py @@ -0,0 +1,97 @@ +import psycopg2.extras +from psycopg2._psycopg import connection + +# swipe create and delete + +def create_swipe( + conn: connection, + username: str, + feed_id: int, + picture_id: int, + value: int +): + with conn.cursor() as cur: + cur.execute( + """ + insert into picrinth.swipes + (username, feed_id, picture_id, value, created_at) + values (%s, %s, %s, %s, now()) + returning id + """, + (username, feed_id, picture_id, value), + ) + conn.commit() + return cur.rowcount > 0 + + +def delete_swipe( + conn: connection, + username: str, + feed_id: int, + picture_id: int +): + with conn.cursor() as cur: + cur.execute( + """ + delete from picrinth.swipes + where username = %s and picture_id = %s and feed_id = %s + """, + (username, picture_id, feed_id), + ) + conn.commit() + return cur.rowcount > 0 + + +# swipe receiving + +def get_swipe( + conn: connection, + username: str, + feed_id: int, + picture_id: int +): + with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur: + cur.execute( + """ + select username, + feed_id, picture_id, + value, created_at + from picrinth.swipes + where username = %s and feed_id = %s and picture_id = %s + """, + (username, feed_id, picture_id), + ) + return cur.fetchone() + + +def get_swipes_by_picture_id( + conn: connection, + feed_id: int, + picture_id: int +): + with conn.cursor() as cur: + cur.execute( + """ + select * + from picrinth.swipes + where feed_id = %s and picture_id = %s + """, + (feed_id, picture_id), + ) + return cur.fetchone() + +def get_swipes_by_user( + conn: connection, + username: str, + feed_id: int +): + with conn.cursor() as cur: + cur.execute( + """ + select * + from picrinth.swipes + where username = %s and feed_id = %s + """, + (username, feed_id), + ) + return cur.fetchone() diff --git a/src/settings/consts.py b/src/settings/consts.py index 8a3aaad..3486aee 100644 --- a/src/settings/consts.py +++ b/src/settings/consts.py @@ -1 +1,15 @@ JOIN_CODE_SYMBOLS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789" # No O + 0, I + 1 + +API_EDITABLE_SETTINGS_LIST = """ +admin_roles ["admin"] + +allow_create_admins_by_admins True + +allow_create_admins True + +allow_create_users True + +allow_create_groups True + +allow_create_pictures True +""" diff --git a/src/settings/settings.py b/src/settings/settings.py index c0969a4..3d1797e 100644 --- a/src/settings/settings.py +++ b/src/settings/settings.py @@ -12,7 +12,7 @@ class Settings(BaseModel): self.allow_create_admins = params["allow_create_admins"] self.allow_create_users = params["allow_create_users"] self.allow_create_groups = params["allow_create_groups"] - self.allow_create_pictures = params["allow_create_groups"] + self.allow_create_pictures = params["allow_create_pictures"] admin_roles: list[str] = ["admin"] allow_create_admins_by_admins: bool = True allow_create_admins: bool = True @@ -61,9 +61,7 @@ def settings_down(): def reset_settings(): global settings, json_path, json_settings_name logger.info("Resetting settings") - print(settings) settings = Settings() - print(settings) with open(json_path + json_settings_name, "w") as f: json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4) logger.info("Wrote settings to the JSON")