diff --git a/alembic/versions/2024_07_07_0006-3038cd0ae3b0_added_moderation_model.py b/alembic/versions/2024_07_07_0006-3038cd0ae3b0_added_moderation_model.py new file mode 100644 index 00000000..894f61f3 --- /dev/null +++ b/alembic/versions/2024_07_07_0006-3038cd0ae3b0_added_moderation_model.py @@ -0,0 +1,39 @@ +"""Added moderation model + +Revision ID: 3038cd0ae3b0 +Revises: 4c13fdf8868d +Create Date: 2024-07-07 00:06:45.747702 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '3038cd0ae3b0' +down_revision = '4c13fdf8868d' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('service_moderation', + sa.Column('target_type', sa.String(length=64), nullable=False), + sa.Column('data', postgresql.JSONB(astext_type=sa.Text()), nullable=False), + sa.Column('log_id', sa.Uuid(), nullable=True), + sa.Column('user_id', sa.Uuid(), nullable=True), + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('created', sa.DateTime(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['service_users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_service_moderation_target_type'), 'service_moderation', ['target_type'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_service_moderation_target_type'), table_name='service_moderation') + op.drop_table('service_moderation') + # ### end Alembic commands ### diff --git a/app/__init__.py b/app/__init__.py index 8392af77..e2dd6dda 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -47,6 +47,7 @@ async def lifespan(app: FastAPI): {"name": "Read"}, {"name": "Related"}, {"name": "Edit"}, + {"name": "Moderation"}, {"name": "Settings"}, {"name": "Schedule"}, {"name": "Upload"}, @@ -84,6 +85,7 @@ async def lifespan(app: FastAPI): from .integrations import router as integrations_router from .collections import router as collections_router from .characters import router as characters_router + from .moderation import router as moderation_router from .companies import router as companies_router from .favourite import router as favourite_router from .settings import router as settings_router @@ -112,6 +114,7 @@ async def lifespan(app: FastAPI): app.include_router(integrations_router) app.include_router(collections_router) app.include_router(characters_router) + app.include_router(moderation_router) app.include_router(companies_router) app.include_router(favourite_router) app.include_router(settings_router) diff --git a/app/constants.py b/app/constants.py index 396a76a7..1d3e24fe 100644 --- a/app/constants.py +++ b/app/constants.py @@ -508,3 +508,11 @@ COLLECTION_PUBLIC = "public" COLLECTION_UNLISTED = "unlisted" COLLECTION_PRIVATE = "private" + +# Moderation types +MODERATION_EDIT_ACCEPTED = "edit_accepted" +MODERATION_EDIT_DENIED = "edit_denied" +MODERATION_EDIT_UPDATED = "edit_updated" +MODERATION_COMMENT_HIDDEN = "comment_hidden" +MODERATION_COLLECTION_DELETED = "collection_deleted" +MODERATION_COLLECTION_UPDATED = "collection_updated" diff --git a/app/errors.py b/app/errors.py index 5c342a1c..2d2313ea 100644 --- a/app/errors.py +++ b/app/errors.py @@ -192,6 +192,9 @@ class ErrorResponse(CustomModel): "system": { "bad-backup-token": ["Bad backup token", 401], }, + "moderation-log": { + "no-access": ["You do not have permission to access", 400], + }, "client": { "already-verified": ["Client is already verified", 400], "not-owner": ["User not owner of the client", 400], diff --git a/app/manga/schemas.py b/app/manga/schemas.py index e8a3d3d2..245dac41 100644 --- a/app/manga/schemas.py +++ b/app/manga/schemas.py @@ -1,3 +1,4 @@ +from pydantic import Field from app.schemas import datetime_pd from app.schemas import ( @@ -23,28 +24,28 @@ class MangaInfoResponse(CustomModel, DataTypeMixin): authors: list[ContentAuthorResponse] magazines: list[MagazineResponse] external: list[ExternalResponse] - start_date: datetime_pd | None - end_date: datetime_pd | None + start_date: datetime_pd | None = Field(examples=[786585600]) + end_date: datetime_pd | None = Field(examples=[1008806400]) genres: list[GenreResponse] - title_original: str | None + title_original: str | None = Field(examples=["Monster"]) stats: ReadStatsResponse - synopsis_en: str | None - synopsis_ua: str | None - media_type: str | None - chapters: int | None - title_en: str | None - title_ua: str | None + synopsis_en: str | None = Field(examples=["..."]) + synopsis_ua: str | None = Field(examples=["..."]) + media_type: str | None = Field(examples=["manga"]) + chapters: int | None = Field(examples=[162]) + title_en: str | None = Field(examples=["Monster"]) + title_ua: str | None = Field(examples=["Монстр"]) updated: datetime_pd synonyms: list[str] comments_count: int - has_franchise: bool - translated_ua: bool - volumes: int | None - status: str | None - image: str | None - year: int | None - scored_by: int - score: float - mal_id: int - nsfw: bool - slug: str + has_franchise: bool = Field(examples=[True]) + translated_ua: bool = Field(examples=[True]) + volumes: int | None = Field(examples=[18]) + status: str | None = Field(examples=["finished"]) + image: str | None = Field(examples=["https://cdn.hikka.io/hikka.jpg"]) + year: int | None = Field(examples=[1994]) + scored_by: int = Field(examples=[99368]) + score: float = Field(examples=[9.16]) + mal_id: int = Field(examples=[1]) + nsfw: bool = Field(examples=[False]) + slug: str = Field(examples=["monster-54bb37"]) diff --git a/app/models/__init__.py b/app/models/__init__.py index 8d2489c8..df2a8a18 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -26,6 +26,8 @@ from .edit.edit import NovelEdit from .edit.edit import Edit +from .moderation.moderation import Moderation + from .comments.comment import CollectionComment from .comments.vote import CommentVoteLegacy from .comments.comment import AnimeComment @@ -113,6 +115,7 @@ "ReadImportHistory", "WatchHistory", "History", + "Moderation", "UserOAuth", "Follow", "User", diff --git a/app/models/moderation/moderation.py b/app/models/moderation/moderation.py new file mode 100644 index 00000000..556decbe --- /dev/null +++ b/app/models/moderation/moderation.py @@ -0,0 +1,20 @@ +from sqlalchemy.dialects.postgresql import JSONB +from ..mixins import CreatedMixin +from sqlalchemy.orm import mapped_column +from sqlalchemy.orm import relationship +from sqlalchemy.orm import Mapped +from sqlalchemy import ForeignKey +from sqlalchemy import String +from ..base import Base +from uuid import UUID + + +class Moderation(Base, CreatedMixin): + __tablename__ = "service_moderation" + + target_type: Mapped[str] = mapped_column(String(64), index=True) + data: Mapped[dict] = mapped_column(JSONB, default={}) + log_id: Mapped[UUID] = mapped_column(nullable=True) + + user_id = mapped_column(ForeignKey("service_users.id")) + user: Mapped["User"] = relationship(foreign_keys=[user_id]) diff --git a/app/moderation/__init__.py b/app/moderation/__init__.py new file mode 100644 index 00000000..5bc0c2e6 --- /dev/null +++ b/app/moderation/__init__.py @@ -0,0 +1,3 @@ +from .router import router + +__all__ = ["router"] diff --git a/app/moderation/dependencies.py b/app/moderation/dependencies.py new file mode 100644 index 00000000..b758f7e6 --- /dev/null +++ b/app/moderation/dependencies.py @@ -0,0 +1,33 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from app.database import get_session +from app.dependencies import auth_required +from app.errors import Abort +from fastapi import Depends + +from app.models.user.user import User + +from .schemas import ModerationSearchArgs + +from app.service import ( + get_user_by_username, +) + + +async def validate_moderation_search_args( + args: ModerationSearchArgs, + session: AsyncSession = Depends(get_session), +): + if args.author: + if not await get_user_by_username(session, args.author): + raise Abort("edit", "author-not-found") + + return args + + +async def validate_moderation_role( + author: User = Depends(auth_required(optional=False)), +): + if author.role not in ["admin", "moderator"]: + raise Abort("moderation-log", "no-access") + + return author diff --git a/app/moderation/router.py b/app/moderation/router.py new file mode 100644 index 00000000..3cd4e9fc --- /dev/null +++ b/app/moderation/router.py @@ -0,0 +1,54 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from fastapi import APIRouter, Depends +from app import constants +from app.database import get_session +from app.models import User +from . import service + + +from .schemas import ( + ModerationPaginationResponse, + ModerationSearchArgs, +) + +from .dependencies import ( + validate_moderation_search_args, + validate_moderation_role, +) + +from app.utils import ( + pagination_dict, + pagination, +) + +from app.dependencies import ( + auth_required, + get_page, + get_size, + get_user, +) + + +router = APIRouter(prefix="/moderation", tags=["Moderation"]) + + +@router.post( + "/log", + response_model=ModerationPaginationResponse, + summary="Moderation log", +) +async def moderation_log( + args: ModerationSearchArgs = Depends(validate_moderation_search_args), + session: AsyncSession = Depends(get_session), + user: User = Depends(validate_moderation_role), + page: int = Depends(get_page), + size: int = Depends(get_size), +): + limit, offset = pagination(page, size) + total = await service.get_moderation_count(session, args) + moderation = await service.get_moderation(session, args, limit, offset) + + return { + "pagination": pagination_dict(total, page, limit), + "list": moderation.all(), + } diff --git a/app/moderation/schemas.py b/app/moderation/schemas.py new file mode 100644 index 00000000..b9c6d285 --- /dev/null +++ b/app/moderation/schemas.py @@ -0,0 +1,54 @@ +from enum import Enum +from pydantic import field_validator +from app import constants +from app.schemas import PaginationResponse, CustomModel +from app.schemas import datetime_pd + + +# Responses +class ModerationResponse(CustomModel): + target_type: str + created: datetime_pd + reference: str + data: dict + + +class ModerationPaginationResponse(CustomModel): + pagination: PaginationResponse + list: list[ModerationResponse] + + +# Enums +class ModerationTypeEnum(str, Enum): + edit_accepted = constants.MODERATION_EDIT_ACCEPTED + edit_denied = constants.MODERATION_EDIT_DENIED + edit_updated = constants.MODERATION_EDIT_UPDATED + comment_hidden = constants.MODERATION_COMMENT_HIDDEN + collection_deleted = constants.MODERATION_COLLECTION_DELETED + collection_updated = constants.MODERATION_COLLECTION_UPDATED + + +# Args +class ModerationSearchArgs(CustomModel): + sort: str = "created:desc" + target_type: ModerationTypeEnum | None = None + author: str | None = None + + @field_validator("sort") + def validate_sort(cls, sort): + valid_orders = ["asc", "desc"] + valid_fields = [ + "created", + ] + + parts = sort.split(":") + + if len(parts) != 2: + raise ValueError(f"Invalid sort format: {sort}") + + field, order = parts + + if field not in valid_fields or order not in valid_orders: + raise ValueError(f"Invalid sort value: {sort}") + + return sort diff --git a/app/moderation/service.py b/app/moderation/service.py new file mode 100644 index 00000000..ba1786ae --- /dev/null +++ b/app/moderation/service.py @@ -0,0 +1,61 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.sql.selectable import Select +from sqlalchemy import asc, select, desc, func + +from app.models import Moderation +from app.service import get_user_by_username +from .schemas import ModerationSearchArgs + + +async def moderation_search_filter( + session: AsyncSession, + args: ModerationSearchArgs, + query: Select, +): + if args.author: + author = await get_user_by_username(session, args.author) + query = query.filter(Moderation.user == author) + + if args.target_type: + query = query.filter(Moderation.target_type == args.target_type) + + return query + + +def build_moderation_order_by(sort: str): + order_mapping = { + "created": Moderation.created, + } + + field, order = sort.split(":") + + order_by = ( + desc(order_mapping[field]) + if order == "desc" + else asc(order_mapping[field]) + ) + + return order_by + + +async def get_moderation_count( + session: AsyncSession, args: ModerationSearchArgs +) -> int: + query = await moderation_search_filter( + session, args, select(func.count(Moderation.id)) + ) + + return await session.scalar(query) + + +async def get_moderation( + session: AsyncSession, args: ModerationSearchArgs, limit: int, offset: int +) -> list[Moderation]: + """Get moderation log""" + + query = await moderation_search_filter(session, args, select(Moderation)) + + query = query.order_by(build_moderation_order_by(args.sort)) + query = query.limit(limit).offset(offset) + + return await session.scalars(query) diff --git a/app/schemas.py b/app/schemas.py index 12aa7803..050abff8 100644 --- a/app/schemas.py +++ b/app/schemas.py @@ -382,19 +382,19 @@ class AnimeResponse(CustomModel, DataTypeMixin): class MangaResponse(CustomModel, DataTypeMixin): - title_original: str | None - media_type: str | None - title_ua: str | None - title_en: str | None - chapters: int | None - volumes: int | None - translated_ua: bool - status: str | None - image: str | None - year: int | None - scored_by: int - score: float - slug: str + title_original: str | None = Field(examples=["Monster"]) + media_type: str | None = Field(examples=["manga"]) + title_ua: str | None = Field(examples=["Монстр"]) + title_en: str | None = Field(examples=["Monster"]) + chapters: int | None = Field(examples=[162]) + volumes: int | None = Field(examples=[18]) + translated_ua: bool = Field(True) + status: str | None = Field(examples=["finished"]) + image: str | None = Field(examples=["https://cdn.hikka.io/hikka.jpg"]) + year: int | None = Field(examples=[1994]) + scored_by: int = Field(examples=[99368]) + score: float = Field(examples=[9.16]) + slug: str = Field(examples=["monster-54bb37"]) class NovelResponse(CustomModel, DataTypeMixin): diff --git a/app/sync/__init__.py b/app/sync/__init__.py index 7e22091b..4d2d22e1 100644 --- a/app/sync/__init__.py +++ b/app/sync/__init__.py @@ -23,6 +23,8 @@ from .history import update_history +from .moderation import update_moderation + from .weights import update_weights from .sitemap import update_sitemap @@ -51,6 +53,7 @@ "update_ranking_all", "aggregator_people", "aggregator_genres", + "update_moderation", "aggregator_anime", "aggregator_manga", "aggregator_novel", diff --git a/app/sync/moderation/__init__.py b/app/sync/moderation/__init__.py new file mode 100644 index 00000000..a2d73922 --- /dev/null +++ b/app/sync/moderation/__init__.py @@ -0,0 +1,78 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from app.models import SystemTimestamp, Log +from app.database import sessionmanager +from sqlalchemy import select, asc +from datetime import datetime +from app import constants + +from .generate import ( + generate_collection_delete, + generate_collection_update, + generate_comment_hide, + generate_edit_accept, + generate_edit_update, + generate_edit_deny, +) + + +async def generate_moderation(session: AsyncSession): + if not ( + system_timestamp := await session.scalar( + select(SystemTimestamp).filter(SystemTimestamp.name == "moderation") + ) + ): + system_timestamp = SystemTimestamp( + **{ + "timestamp": datetime(2024, 1, 13), + "name": "moderation", + } + ) + + logs = await session.scalars( + select(Log) + .filter( + Log.log_type.in_( + [ + constants.LOG_EDIT_ACCEPT, + constants.LOG_EDIT_DENY, + constants.LOG_EDIT_UPDATE, + constants.LOG_COMMENT_HIDE, + constants.LOG_COLLECTION_DELETE, + constants.LOG_COLLECTION_UPDATE, + ] + ) + ) + .filter(Log.created > system_timestamp.timestamp) + .order_by(asc(Log.created)) + ) + + for log in logs: + system_timestamp.timestamp = log.created + + if log.log_type == constants.LOG_EDIT_ACCEPT: + await generate_edit_accept(session, log) + + if log.log_type == constants.LOG_EDIT_DENY: + await generate_edit_deny(session, log) + + if log.log_type == constants.LOG_EDIT_UPDATE: + await generate_edit_update(session, log) + + if log.log_type == constants.LOG_COMMENT_HIDE: + await generate_comment_hide(session, log) + + if log.log_type == constants.LOG_COLLECTION_DELETE: + await generate_collection_delete(session, log) + + if log.log_type == constants.LOG_COLLECTION_UPDATE: + await generate_collection_update(session, log) + + session.add(system_timestamp) + await session.commit() + + +async def update_moderation(): + """Generate moderation log from logs""" + + async with sessionmanager.session() as session: + await generate_moderation(session) diff --git a/app/sync/moderation/generate/__init__.py b/app/sync/moderation/generate/__init__.py new file mode 100644 index 00000000..f57da93f --- /dev/null +++ b/app/sync/moderation/generate/__init__.py @@ -0,0 +1,18 @@ +from .edit_accept import generate_edit_accept +from .edit_deny import generate_edit_deny +from .edit_update import generate_edit_update + +from .comment_hide import generate_comment_hide + +from .collection_delete import generate_collection_delete +from .collection_update import generate_collection_update + + +__all__ = [ + "generate_collection_delete", + "generate_collection_update", + "generate_comment_hide", + "generate_edit_accept", + "generate_edit_update", + "generate_edit_deny", +] diff --git a/app/sync/moderation/generate/collection_delete.py b/app/sync/moderation/generate/collection_delete.py new file mode 100644 index 00000000..527ddd99 --- /dev/null +++ b/app/sync/moderation/generate/collection_delete.py @@ -0,0 +1,43 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from app.models import Moderation, Log +from app import constants +from .. import service + + +async def generate_collection_delete(session: AsyncSession, log: Log): + target_type = constants.MODERATION_COLLECTION_DELETED + + if not (collection := await service.get_collection(session, log.target_id)): + return + + if not collection.author: + return + + if collection.author_id == log.user_id: + return + + if await service.get_moderation( + session, + log.user_id, + log.id, + target_type, + ): + return + + await session.refresh(log, attribute_names=["user"]) + + moderation = Moderation( + **{ + "target_type": target_type, + "user_id": log.user_id, + "created": log.created, + "log_id": log.id, + "data": { + "username": log.user.username, + "avatar": log.user.avatar, + "collection_id": str(collection.id), + }, + } + ) + + session.add(moderation) diff --git a/app/sync/moderation/generate/collection_update.py b/app/sync/moderation/generate/collection_update.py new file mode 100644 index 00000000..a392ab09 --- /dev/null +++ b/app/sync/moderation/generate/collection_update.py @@ -0,0 +1,45 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from app.models import Moderation, Log +from app import constants +from .. import service + + +async def generate_collection_update(session: AsyncSession, log: Log): + target_type = constants.MODERATION_COLLECTION_UPDATED + + if not (collection := await service.get_collection(session, log.target_id)): + return + + if not collection.author: + return + + if collection.author_id == log.user_id: + return + + if await service.get_moderation( + session, + log.user_id, + log.id, + target_type, + ): + return + + await session.refresh(log, attribute_names=["user"]) + + moderation = Moderation( + **{ + "target_type": target_type, + "user_id": log.user_id, + "created": log.created, + "log_id": log.id, + "data": { + "updated_collection": log.data["updated_collection"], + "old_collection": log.data["old_collection"], + "username": log.user.username, + "avatar": log.user.avatar, + "collection_id": str(collection.id), + }, + } + ) + + session.add(moderation) diff --git a/app/sync/moderation/generate/comment_hide.py b/app/sync/moderation/generate/comment_hide.py new file mode 100644 index 00000000..b117c72b --- /dev/null +++ b/app/sync/moderation/generate/comment_hide.py @@ -0,0 +1,45 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from app.models import Moderation, Log +from app import constants +from .. import service + + +async def generate_comment_hide(session: AsyncSession, log: Log): + target_type = constants.MODERATION_COMMENT_HIDDEN + + if not (comment := await service.get_comment(session, log.target_id)): + return + + if not comment: + return + + if comment.author_id == comment.hidden_by_id: + return + + if await service.get_moderation( + session, + log.user_id, + log.id, + target_type, + ): + return + + await session.refresh(log, attribute_names=["user"]) + + moderation = Moderation( + **{ + "target_type": target_type, + "user_id": log.user_id, + "created": log.created, + "log_id": log.id, + "data": { + "content_type": comment.content_type, + "content_slug": comment.preview["slug"], + "comment_path": comment.path, + "username": log.user.username, + "avatar": log.user.avatar, + }, + } + ) + + session.add(moderation) diff --git a/app/sync/moderation/generate/edit_accept.py b/app/sync/moderation/generate/edit_accept.py new file mode 100644 index 00000000..2dc32034 --- /dev/null +++ b/app/sync/moderation/generate/edit_accept.py @@ -0,0 +1,44 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from app.models import Moderation, Log +from app import constants +from .. import service + + +async def generate_edit_accept(session: AsyncSession, log: Log): + target_type = constants.MODERATION_EDIT_ACCEPTED + + if not (edit := await service.get_edit(session, log.target_id)): + return + + if not edit.author: + return + + if edit.author_id == edit.moderator_id: + return + + if await service.get_moderation( + session, + log.user_id, + log.id, + target_type, + ): + return + + await session.refresh(log, attribute_names=["user"]) + + moderation = Moderation( + **{ + "target_type": target_type, + "user_id": log.user_id, + "created": log.created, + "log_id": log.id, + "data": { + "description": edit.description, + "edit_id": edit.edit_id, + "username": log.user.username, + "avatar": log.user.avatar, + }, + } + ) + + session.add(moderation) diff --git a/app/sync/moderation/generate/edit_deny.py b/app/sync/moderation/generate/edit_deny.py new file mode 100644 index 00000000..fcc8aa7a --- /dev/null +++ b/app/sync/moderation/generate/edit_deny.py @@ -0,0 +1,44 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from app.models import Moderation, Log +from app import constants +from .. import service + + +async def generate_edit_deny(session: AsyncSession, log: Log): + target_type = constants.MODERATION_EDIT_DENIED + + if not (edit := await service.get_edit(session, log.target_id)): + return + + if not edit.author: + return + + if edit.author_id == edit.moderator_id: + return + + if await service.get_moderation( + session, + log.user_id, + log.id, + target_type, + ): + return + + await session.refresh(log, attribute_names=["user"]) + + moderation = Moderation( + **{ + "target_type": target_type, + "user_id": log.user_id, + "created": log.created, + "log_id": log.id, + "data": { + "description": edit.description, + "username": log.user.username, + "avatar": log.user.avatar, + "edit_id": edit.edit_id, + }, + } + ) + + session.add(moderation) diff --git a/app/sync/moderation/generate/edit_update.py b/app/sync/moderation/generate/edit_update.py new file mode 100644 index 00000000..022c8e01 --- /dev/null +++ b/app/sync/moderation/generate/edit_update.py @@ -0,0 +1,45 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from app.models import Moderation, Log +from app import constants +from .. import service + + +async def generate_edit_update(session: AsyncSession, log: Log): + target_type = constants.MODERATION_EDIT_UPDATED + + if not (edit := await service.get_edit(session, log.target_id)): + return + + if not edit.author: + return + + if edit.author_id == log.user_id: + return + + if await service.get_moderation( + session, + log.user_id, + log.id, + target_type, + ): + return + + await session.refresh(log, attribute_names=["user"]) + + moderation = Moderation( + **{ + "target_type": target_type, + "user_id": log.user_id, + "created": log.created, + "log_id": log.id, + "data": { + "updated_edit": log.data["updated_edit"], + "old_edit": log.data["old_edit"], + "username": log.user.username, + "avatar": log.user.avatar, + "edit_id": edit.edit_id, + }, + } + ) + + session.add(moderation) diff --git a/app/sync/moderation/service.py b/app/sync/moderation/service.py new file mode 100644 index 00000000..8502cacc --- /dev/null +++ b/app/sync/moderation/service.py @@ -0,0 +1,39 @@ +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, desc +from app.models import Moderation, Edit, Comment, Collection +from uuid import UUID + + +async def get_moderation( + session: AsyncSession, + user_id: UUID, + log_id: UUID, + target_type: str | None = None, +): + query = select(Moderation).filter( + Moderation.user_id == user_id, + Moderation.log_id == log_id, + ) + + if target_type: + query = query.filter( + Moderation.target_type == target_type, + ) + + return await session.scalar(query.order_by(desc(Moderation.created))) + + +async def get_edit(session, content_id): + return await session.scalar(select(Edit).filter(Edit.id == content_id)) + + +async def get_comment(session, content_id): + return await session.scalar( + select(Comment).filter(Comment.id == content_id) + ) + + +async def get_collection(session, content_id): + return await session.scalar( + select(Collection).filter(Collection.id == content_id) + ) diff --git a/sync.py b/sync.py index 7ecd6925..48b1c63a 100644 --- a/sync.py +++ b/sync.py @@ -7,6 +7,7 @@ delete_expired_token_requests, update_notifications, update_ranking_all, + update_moderation, update_activity, update_schedule, update_ranking, @@ -24,6 +25,7 @@ def init_scheduler(): scheduler.add_job(delete_expired_token_requests, "interval", seconds=30) scheduler.add_job(update_notifications, "interval", seconds=10) + scheduler.add_job(update_moderation, "interval", seconds=10) scheduler.add_job(update_ranking_all, "interval", hours=1) scheduler.add_job(update_activity, "interval", seconds=10) scheduler.add_job(update_schedule, "interval", minutes=5)