diff --git a/.env.example b/.env.example index cd69276..69742f0 100644 --- a/.env.example +++ b/.env.example @@ -4,3 +4,4 @@ LOGGER__APP_NAME="tgex-backend" LOGGER__PRETTY_CONSOLE=true TELEGRAM__TOKEN=your_bot_token_here JWT__SECRET_KEY=your_secret_key_here_please_generate_a_strong_random_string +PARSER__URL=http://localhost:8080 \ No newline at end of file diff --git a/migrations/models/0_20251215193344_init.py b/migrations/models/0_20251215212038_init.py similarity index 75% rename from migrations/models/0_20251215193344_init.py rename to migrations/models/0_20251215212038_init.py index 82446b6..9fde9ac 100644 --- a/migrations/models/0_20251215193344_init.py +++ b/migrations/models/0_20251215212038_init.py @@ -10,9 +10,9 @@ async def upgrade(db: BaseDBAsyncClient) -> str: "created_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted_at" TIMESTAMPTZ, - "telegram_id" BIGINT UNIQUE, - "title" VARCHAR(255), - "username" VARCHAR(255), + "telegram_id" BIGINT NOT NULL UNIQUE, + "title" VARCHAR(255) NOT NULL, + "username" VARCHAR(255) NOT NULL UNIQUE, "access_hash" BIGINT, "pts" INT ); @@ -253,65 +253,65 @@ MODELS_STATE = ( "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" - "HuO2Ldj/vOfUe41vuO1vYR3o2bjX7/H/FMJEI=" + "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" + "37jnCt9x2t7SO8Gzcb/f4/gXYk2A==" ) diff --git a/migrations/models/1_20251215220358_update.py b/migrations/models/1_20251215220358_update.py new file mode 100644 index 0000000..7f19adc --- /dev/null +++ b/migrations/models/1_20251215220358_update.py @@ -0,0 +1,90 @@ +from tortoise import BaseDBAsyncClient + +RUN_IN_TRANSACTION = True + + +async def upgrade(db: BaseDBAsyncClient) -> str: + return """ + COMMENT ON COLUMN "placement"."status" IS 'ACTIVE: active +ARCHIVED: archived';""" + + +async def downgrade(db: BaseDBAsyncClient) -> str: + return """ + COMMENT ON COLUMN "placement"."status" IS 'PENDING: pending +ACTIVE: active +DELETED: deleted +ARCHIVED: archived';""" + + +MODELS_STATE = ( + "eJztXWtv2zgW/SuGP3WAbJE4zqPBYgHHUVNNHduwnXR2mkJQZMbRRpY8eiQNiv73JfWkJE" + "rRy5Zs3y9Fw5elc0mK5/Dy8ld7qc2RYnzsP4mqipT2RetXWxWXCP8nmnXQaourVZBBEkzx" + "QbHLSlShB8PURcnEyY+iYiCcNEeGpMsrU9ZUnKpaikISNQkXlNVFkGSp8j8WEkxtgcwnpO" + "OM7z9wsqzO0U9keH+unoVHGSnz0MPKc/Lbdrpgvq3stNtb/uqzXZL83IMgaYq1VIPSqzfz" + "SVP94pYlzz+SOiRvgVSkiyaaU69BntJ9YS/JeWKcYOoW8h91HiTM0aNoKQSM9r8fLVUiGL" + "TsXyL/dP/TzgGPpKkEWlk1CRa/fjtvFbyzndomP9X/0pt8OD79w35LzTAXup1pI9L+bVcU" + "TdGpauMaACnpiLy2IJpxQK9wjikvERvUcM0IuHO36kfvP0VA9hIClIMe5sHswVcM0zZ+h/" + "lIVd5cC6ZgPONvuOmsdzMmb7I0jH8UG6LejCM5HTv1LZL6wTGJhseHM278Rlrf+NmXFvmz" + "9fdoyEUN55eb/d0mzyRapiao2qsgzqnO5qV6wOCSgWGt1bygYcM1wbC1GtZ9+MCueC5Gxe" + "warlmBXd2n3aBZt8SM3munDlATm2Ohi0uB9S27lBe8arLtGKkYMSSGaj0ftJIDckF+5V+f" + "Op3j47PO4fHp+Un37Ozk/PAcl7UfKZ51lmLuS/6aH87C1iMJv8MYy6aC4ujiNY6egK1XIY" + "IqfpWGTnRL8aegIHVhPuE/OycnKZjd9Sb2SgGXivT7oZvVcfLCIFoG0u3/58CRrlMNlGvv" + "oOsHUpQkZBjCk2g85RvxkYqFRvzmJ+t6hvzKNOLgJiLrlt4uRDtH3bPu+fFp1wfST0nDz8" + "GKUKrHZ4oLkIQHUXp+FfW5EMqhQFVECS2RysL20q37+esEKaL9SnFEXXI59tpp5mT62+se" + "XiprAUbWAWVhwE1sMwK69j8klQbBaWWbcbB0Cc/JSMDDQxVcTaQsKm6beKSolBSzRQiR+U" + "TraEkzTDxr2VlGU0RVXNhPTX6b/JInThHNQX5BbZZw5eUdpCpXdCmQrkC6AoUDpCswLEhX" + "IF1ll67yqgHVKgE7IaqY6CdjFMxwapLo95PV9xsLYVp35/6ahXq6B9SHm95ff4R6+2A0vP" + "aKU8D2B6PLCJ6GKZoWY+1NOiWnWksbVR4/oqhKKIZuUHtz+LbxytNdiIYxbvf6M/6Ou2g5" + "Be5Vgg5OuMIpmBzgJKd75OzG5xk68XliFz6PduBAEsA4WiqjMyeLLoyq1ajYWXA/LDEvVK" + "C/MIg0cwcgmRKEa1VJDeLwVSyyliECAWyvmv5srHAfyglctN6eQBdT/BhIxmH8rOlIXqhf" + "0Vts6mQrF9/otpoKX0yuwMm6+OrT9Fgfwe/pLD5toHvTfu+Ka7MGcQUIVqqI1YRfeHJiow" + "eKc70q2kBbyOpMe0Zqm6GjUbkHaUqaQsoJpl8QxDQQ00BzATENDAtiGohpOfzAvC9oZh8l" + "rwI41rgQop8rGVukwFgI19zOOW6XBoNlFPtUGTCfNciEek5hhqqyXk1mg3YrLsoQNCpQE2" + "7dZhqL2rtaAtUt8goJ66TPgaTAYM8hvSGZPK9CxYA6A3Xe1tXHzjAsoM47aligzru61PS/" + "ogKBPK8x47W3c6DukkUl1zM+svZVNDHBl8CrEDHdI6mxbePvanR7OeBa4wnX56f8aBg2j5" + "1JknCC7CyEJ1xvEJFCJG3pLSuzuhZRVQopSo3CcC3ORbL6ghEXFFl9drCJoZvNzYjVTs0O" + "Xe0x7lZ8/6K1sh4UWbpXe+PxZHTXG1y0MHvRtRfRPn9Qs7MRBVwepTRSbVt858JQnhx1Mo" + "CJSyXCaeeBu1ytPdiwHvyHzu8xl1B7L53mvBM8OTXGSLU98f1ir5bdo2p5/Q4T6u8jlDg7" + "L3pBlRKAvb/WaiZe4OhaBDZwdK1qTwWcNPM6aaZ+OSrAsdLDzXXhmPBFfB9R+hhyWSCppr" + "YWycjq7H0AwXM9CmEBz3W22JZ7RswWzaKupcv7ozhYmYUwm3Kz1vB2MMjmsB6iaHFU8/is" + "T6mmmqlQ1OK2bvcz1pa72/9Sdtu9EpVutH9vh2f9JTIM/OTkrx+wCb+WpTFswu/4Xi1swu" + "+oYWETfle3bKnPXsyQiYJ2uNLmdOySo7FiKRuCQFS7T+vNDI+6tvQpefEJhtEMzDZ1O4gU" + "2rrYyw2LFDkUZLyQ+JRBvGvqmfXaZpP00JgvMno1hCfZMDVdRhWES70jDX6x23tr9Mdu85" + "pICJoEfSQKX7pWItDme6teOQF1ZA3T/QGoI7tOokEd2VHDgjqyq3zF+ZLm9feL1NpXfeQR" + "mdJToVEQrrmZ2W1zxG/rRkFt/nKNMlsJ96WN7tQ3l6knbdXXfSLc9Qljka/AXSyFc1GFKt" + "6ijnqFUFoHbFIDDYPVOtAwMCzQsF1fgO7yeTd+6KXIaqPPwMHeHZwCqZVGgdP4QWmncdg6" + "XsfWsXf+oeROabVnQWrZOd73wO+JF0xWeLHklkGyVumGhoWl30RgSxFxaGOtfc/8Oy0aue" + "vTH6DggIIDRB8UnP01LCg4oOBsn4LTSLUGYnaAWgNqTaPUBricrqT7QEWX07mqT4WMvFIt" + "bOeIuYfOO/ycAjEjTafj2VTteRH+IfC+AO4OFA+4OxgWuHvZObBJZtxV7k4+2qrLwyMBsw" + "e94ZBwdbeIFzLbpu92yGySNuH+5PozkqYjsuYkafxQwAWvJ9x0Snw1BFyWoGzcq/3RzXjA" + "2cUlbbmyO28RCeDoKIMGcHSUKAKQrFjQWPKSQu7A/NGKEKAfAvTbr7mOwA/gWVRIq2JRlM" + "xCH6PunkCYdlAjuglcVnWpfNe+LumF0V3A2WiTzkbrFGtCESsZKk00omWyPGNES0LgAdBc" + "gJqD5gKGBc0FNJfsmkuzr8yqeAUFN2aV8j+5HU5vL6f9CX9JFChLdRdhDwVFqCzop4Afx5" + "5+oiJfnHh1mJ7qjgjhX51R9BKqvZIcGFfJPSA9J3axinsCXppeQ58SKavVVHvipPY7bbKK" + "NEG3qgDEWwM1c4mRFb/YIGuSTjPDD7LQxeUUL0dQmyHUhAscpCk1pltUMPyyoNWAVgOUHr" + "QaMCxoNUCGspMh/1PK+pZdyovEOJGRitXEiXz/g1ZyQDpRIj91OsfHZ53D49Pzk+7Z2cn5" + "oR8uMp6VFjfykr8moSND1ovHkvRXKUXlG9aidY3qDRPjdn/C9Yh2I3zr8TN+eC18G02+Ts" + "e9PnfR8u9QfBVlExtA8I8L3KuxenidMBxyA0Ytd5eRUWfYu2H9DEGYUZp4bjBKkztM7tXx" + "ZET8o1iv4Tnpx96iiBbVOc2gRXWiYz/QokhW1H9HZV/b8ud0NEzy31ETbm65VbFdv89lyT" + "xoKbJh/libFhgsQR8sWcHAGh/Jz65pFUqwCE2+MSefqD9PZFYlDVyyqHs9vMlmpQy65LHV" + "ZJZkeSWAGwE3giU0cCMwLHAj4EbAjZrEjcgixf4/kx4lTH9UnS3xSo9Qg5OTLNzg5CSZHJ" + "C8yI0Fsm6YQl4sw7UATQ9NRSwAZqjSfmOZgz1RoGsLGSOmPaOyIecGpKUZaaiZq5/Ec+2M" + "3U278ZJ4RL2gtxURe3PY9hMrCYgflYO3W9tiTKjIHAAMGxiyYKgKluq8BDYGyjrlqSC6DU" + "OjCoW+SRaqQjIrqFWgVsXBBVED1CowLKhVoFYlDtC8ZLUUT61jCDaIqELMfIiZz8DCcR8o" + "i0SV0Q7rwQHuDohCAtQcqHlt1NztLmkEPehRGWi6KzRVz9bjd4aS7gEhK4HIA98DIg+GBS" + "K/D0R+C4/7r5A6J+aJGbA95oZX/PD6ouUWuVd7/T43tuNNipKEVqYTsvJu9NWJWPmiPRc7" + "+1/xHRTOGmcuPLzlPOocq7gnR52jXj05YaOq7CFgcHVHVcfq4eqOg9JXdwRTWAUgbv2x+t" + "iE/j6A3tGLfYeOmtSbFIsgrBil6SLvn7IJS1mgiYAmAppIozgXaCJ7Y1jQREATaYwmkjkE" + "Ij+84209xF1o3quXg1HflkMeFE0qKIekHc3x5JCzRDnkLBYHERg9MHpg9M2gVfkZPRDS0o" + "SU8m9B+lI2jPKnckI0c+y32sxVYjbXH/8lBEPSVpV5vIQBmpKmtwyljekZVD96T9kId7ms" + "GoewCldbm9xBLyCCnwTZA2QPYMcge4BhQfbYddkj/KUtIn2EW6g7hmDv6oYfCp9vB4OLlj" + "hfyqrwiMv5MfmmwoTrXflx+AyBjFAq99uEn3FU9qsum1T8P6+6fzrDrR/kuw0EBdwWxoNe" + "n7vhhsET+McavGcISnhPERRxW+kNe4P/zvi+1wherilvpix5bQT5pOeMbmdCf8D3v07pkq" + "8ytpBlCpIiS8/kelYnWKL/ak6ERP/NvFzvxbxs+4mKyEadbpZjNd3kUzXdqHLEXM/lV0P2" + "T03KIokIFRH7NfjQ166NbMPmc5TTZmdsPgcuQNscbl4HecN/2b/t9D//L9jNBloHq3+gdW" + "BYoHVA64DWAa1rPK0LL+MKeWWEWqi7D7u90e+GQf8KOhbVX6iO4tvLN1QRC33KYKBPifb5" + "xDZPzkv6qDrr5dgb/DgUctkApQKUClAqekiXpac2Q5Nwcw7S1AcxKNOYkH6JEbuzBup27V" + "hOHyjJb5w43Z2j7ln3/Pi064fn9lPSXP+8CNzJssAL0pNXpmz0qCp7Hk2K/pyQoZEDRLf4" + "dgJ4dHiYAUBcKvnqbZLHuO+IdT/uO/cdqbXed7S2j/Bu3Gz0+/9WjBjY" +) diff --git a/src/adapter/parser.py b/src/adapter/parser.py new file mode 100644 index 0000000..28367b7 --- /dev/null +++ b/src/adapter/parser.py @@ -0,0 +1,63 @@ +import logging +from typing import Any + +import httpx +from pydantic import BaseModel + +log = logging.getLogger(__name__) + + +class FetchChannelResponse(BaseModel): + id: str + telegram_id: int + username: str + title: str + access_hash: int + pts: int + + +class ParserClient: + def __init__(self, base_url: str, timeout: float = 5.0): + self.base_url = base_url.rstrip('/') + self.timeout = timeout + + async def fetch_telegram_channel(self, username: str) -> FetchChannelResponse | None: + """ + Проверяет существование канала в Telegram и получает его метаданные. + + Returns: + FetchChannelResponse если канал найден + None если канал не найден (404) + + Raises: + httpx.HTTPError для других ошибок (таймаут, network error, 5xx) + """ + # Убираем @ если пользователь его указал + username = username.lstrip('@') + + url = f'{self.base_url}/fetch-telegram-channel' + params = {'username': username} + + async with httpx.AsyncClient(timeout=self.timeout) as client: + try: + response = await client.get(url, params=params) + + if response.status_code == 404: + log.info('Channel @%s not found in Telegram', username) + return None + + response.raise_for_status() + data: dict[str, Any] = response.json() + return FetchChannelResponse.model_validate(data) + + except httpx.HTTPStatusError as e: + if e.response.status_code == 404: + return None + log.error('Parser HTTP error for @%s: %s', username, e) + raise + except httpx.TimeoutException: + log.error('Parser timeout for @%s', username) + raise + except Exception as e: + log.error('Parser unexpected error for @%s: %s', username, e) + raise diff --git a/src/config.py b/src/config.py index 4338ed6..9ff6ff6 100644 --- a/src/config.py +++ b/src/config.py @@ -12,6 +12,10 @@ class AppConfig(BaseModel): URL: str +class ParserConfig(BaseModel): + URL: str + + class Settings(BaseSettings): model_config = SettingsConfigDict(env_file='.env', case_sensitive=False, env_nested_delimiter='__') @@ -20,6 +24,7 @@ class Settings(BaseSettings): logger: LoggerConfig telegram: TelegramConfig jwt: JWTConfig + parser: ParserConfig settings: Settings = load_settings(Settings) diff --git a/src/controller/http/creatives.py b/src/controller/http/creatives.py index 717040c..93b286a 100644 --- a/src/controller/http/creatives.py +++ b/src/controller/http/creatives.py @@ -1,7 +1,7 @@ import uuid from typing import Annotated -from fastapi import Depends +from fastapi import Depends, Query from fastapi.routing import APIRouter from src import deps, dto @@ -44,11 +44,12 @@ async def get_creative( @creatives_router.post('') async def create_creative( - request: dto.CreateCreativeInput, workspace_id: uuid.UUID, + request: dto.CreateCreativeInput, current_user: Annotated[JWTPayload, Depends(deps.get_current_user)], + project_id: uuid.UUID = Query(), ) -> dto.CreativeOutput: - return await deps.get_usecase().create_creative(request, current_user.user_id, workspace_id) + return await deps.get_usecase().create_creative(request, project_id, current_user.user_id, workspace_id) @creatives_router.patch('/{creative_id}') diff --git a/src/domain/__init__.py b/src/domain/__init__.py index 0bf54f7..66c6728 100644 --- a/src/domain/__init__.py +++ b/src/domain/__init__.py @@ -49,6 +49,7 @@ __all__ = ( 'ChannelNotFound', 'ChannelAlreadyExists', 'ChannelNoAdminRights', + 'TelegramChannelNotFound', 'CreativeNotFound', 'CreativeInUse', 'PlacementNotFound', @@ -70,6 +71,7 @@ from .error import ( ProjectNotFound, PurchasePlanChannelNotFound, PurchasePlanNotFound, + TelegramChannelNotFound, UserByUsernameNotFound, UserNotFound, WorkspaceAccessDenied, diff --git a/src/domain/channel.py b/src/domain/channel.py index 5c08d92..e8e4455 100644 --- a/src/domain/channel.py +++ b/src/domain/channel.py @@ -4,9 +4,9 @@ from .base import TimestampedModel class Channel(TimestampedModel): - telegram_id = fields.BigIntField(null=True, unique=True, index=True) - title = fields.CharField(null=True, max_length=255) - username = fields.CharField(null=True, max_length=255, index=True) + telegram_id = fields.BigIntField(unique=True, index=True) + title = fields.CharField(max_length=255) + username = fields.CharField(max_length=255, unique=True, index=True) access_hash = fields.BigIntField(null=True) pts = fields.IntField(null=True) diff --git a/src/domain/error.py b/src/domain/error.py index 9d0b1f1..051f3ab 100644 --- a/src/domain/error.py +++ b/src/domain/error.py @@ -31,6 +31,12 @@ def ChannelNotFound(channel_id: uuid.UUID | None = None) -> HTTPException: return HTTPException(status.HTTP_404_NOT_FOUND, f'Channel {channel_id} not found') +def TelegramChannelNotFound(username: str) -> HTTPException: + return HTTPException( + status.HTTP_404_NOT_FOUND, f'Telegram channel @{username} not found or is not a public channel' + ) + + def ProjectNotFound(project_id: uuid.UUID | None = None) -> HTTPException: if project_id is None: return HTTPException(status.HTTP_404_NOT_FOUND, 'Project not found') diff --git a/src/domain/placement.py b/src/domain/placement.py index fb2cd18..b649b70 100644 --- a/src/domain/placement.py +++ b/src/domain/placement.py @@ -15,9 +15,7 @@ if TYPE_CHECKING: class PlacementStatus(str, enum.Enum): - PENDING = 'pending' # Ожидается публикация поста ACTIVE = 'active' # Пост найден и отслеживается - DELETED = 'deleted' # Пост удален из канала ARCHIVED = 'archived' # Вручную архивировано diff --git a/src/dto/creative.py b/src/dto/creative.py index 708a3df..48aa6c0 100644 --- a/src/dto/creative.py +++ b/src/dto/creative.py @@ -37,7 +37,6 @@ class GetCreativeInput(pydantic.BaseModel): class CreateCreativeInput(pydantic.BaseModel): name: str text: str - project_id: uuid.UUID class UpdateCreativeInput(pydantic.BaseModel): diff --git a/src/dto/placement.py b/src/dto/placement.py index 4d47d3f..80e4ceb 100644 --- a/src/dto/placement.py +++ b/src/dto/placement.py @@ -22,9 +22,6 @@ class PlacementOutput(pydantic.BaseModel): invite_link: str status: domain.PlacementStatus subscriptions_count: int - views_count: int | None - views_availability: domain.PostViewsAvailability - last_views_fetch_at: datetime.datetime | None created_at: datetime.datetime diff --git a/src/dto/purchase_plan.py b/src/dto/purchase_plan.py index 42da616..bcaffd6 100644 --- a/src/dto/purchase_plan.py +++ b/src/dto/purchase_plan.py @@ -26,7 +26,7 @@ class GetPurchasePlanChannelsOutput(pydantic.BaseModel): class AttachChannelToPurchasePlanInput(pydantic.BaseModel): - username: str + username: str = pydantic.Field(pattern=r'^[^@]') status: PurchasePlanChannelStatus | None = None planned_cost: float | None = None comment: str | None = None diff --git a/src/main.py b/src/main.py index 4855302..1e6ce8f 100644 --- a/src/main.py +++ b/src/main.py @@ -8,6 +8,7 @@ from shared import logger from shared.worker_base import WorkerConfig from src import deps from src.adapter.jwt import JWT +from src.adapter.parser import ParserClient from src.adapter.postgres import Postgres from src.adapter.telegram_bot import TelegramBot from src.config import settings @@ -36,11 +37,13 @@ logger.init(settings.logger) postgres = Postgres(settings.db) telegram = TelegramBot(settings.telegram, telegram_callback_router) jwt_encoder = JWT(settings.jwt) +parser_client = ParserClient(base_url=settings.parser.URL) usecase = Usecase( database=postgres, telegram_bot=telegram, jwt_encoder=jwt_encoder, + parser_client=parser_client, ) deps.set_usecase(usecase) diff --git a/src/usecase/__init__.py b/src/usecase/__init__.py index 744e231..5a56b92 100644 --- a/src/usecase/__init__.py +++ b/src/usecase/__init__.py @@ -279,11 +279,22 @@ class JWTEncoder(typing.Protocol): def encode_access_token(self, user_id: UUID, telegram_id: int, username: str | None = None) -> str: ... +class FetchChannelResponse(typing.Protocol): + telegram_id: int + username: str + title: str + + +class ParserClient(typing.Protocol): + async def fetch_telegram_channel(self, username: str) -> FetchChannelResponse | None: ... + + @dataclass class Usecase: database: Database telegram_bot: TelegramBotWriter jwt_encoder: JWTEncoder + parser_client: ParserClient async def ensure_workspace_access(self, workspace_id: UUID, user_id: UUID) -> domain.Workspace: workspace = await self.database.get_workspace_for_user(workspace_id, user_id) diff --git a/src/usecase/creative/create_creative.py b/src/usecase/creative/create_creative.py index 4028b27..0de7724 100644 --- a/src/usecase/creative/create_creative.py +++ b/src/usecase/creative/create_creative.py @@ -11,19 +11,19 @@ log = logging.getLogger(__name__) async def create_creative( - self: 'Usecase', input: dto.CreateCreativeInput, user_id: uuid.UUID, workspace_id: uuid.UUID + self: 'Usecase', input: dto.CreateCreativeInput, project_id: uuid.UUID, user_id: uuid.UUID, workspace_id: uuid.UUID ) -> dto.CreativeOutput: await self.ensure_workspace_permission( workspace_id, user_id, domain.PermissionKey.PROJECTS_WRITE, - for_project_id=input.project_id, + for_project_id=project_id, ) - project = await self.database.get_project(workspace_id, project_id=input.project_id) + project = await self.database.get_project(workspace_id, project_id=project_id) if not project: - log.warning('User %s attempted to create creative for unavailable project %s', user_id, input.project_id) - raise domain.ProjectNotFound(input.project_id) + log.warning('User %s attempted to create creative for unavailable project %s', user_id, project_id) + raise domain.ProjectNotFound(project_id) creative = domain.Creative( name=input.name, diff --git a/src/usecase/placement/create_placement.py b/src/usecase/placement/create_placement.py index 27978ba..bb33af3 100644 --- a/src/usecase/placement/create_placement.py +++ b/src/usecase/placement/create_placement.py @@ -53,7 +53,7 @@ async def create_placement( comment=input.comment, invite_link_type=input.invite_link_type, invite_link=invite_link, - status=domain.PlacementStatus.PENDING, + status=domain.PlacementStatus.ACTIVE, ) async with self.database.transaction(): @@ -78,8 +78,5 @@ async def create_placement( invite_link=placement.invite_link, status=placement.status, subscriptions_count=placement.subscriptions_count, - views_count=None, - views_availability=domain.PostViewsAvailability.UNKNOWN, - last_views_fetch_at=None, created_at=placement.created_at, ) diff --git a/src/usecase/placement/fetch_placement_post_cycle.py b/src/usecase/placement/fetch_placement_post_cycle.py index 99b8789..107e756 100644 --- a/src/usecase/placement/fetch_placement_post_cycle.py +++ b/src/usecase/placement/fetch_placement_post_cycle.py @@ -10,7 +10,7 @@ log = logging.getLogger(__name__) async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) -> None: - log.info('Starting fetch_placement_post_cycle') + log.debug('Starting fetch_placement_post_cycle') # Получаем все placement без привязанного поста placements_without_post = ( diff --git a/src/usecase/placement/get_placement.py b/src/usecase/placement/get_placement.py index b3826a3..4ee265c 100644 --- a/src/usecase/placement/get_placement.py +++ b/src/usecase/placement/get_placement.py @@ -54,8 +54,5 @@ async def get_placement(self: 'Usecase', input: dto.GetPlacementInput) -> dto.Pl invite_link=placement.invite_link, status=placement.status, subscriptions_count=placement.subscriptions_count, - views_count=views_count, - views_availability=views_availability, - last_views_fetch_at=last_views_fetch_at, created_at=placement.created_at, ) diff --git a/src/usecase/placement/get_placements.py b/src/usecase/placement/get_placements.py index a2cc738..793d1df 100644 --- a/src/usecase/placement/get_placements.py +++ b/src/usecase/placement/get_placements.py @@ -28,25 +28,12 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto. allowed_project_ids=allowed_project_ids, ) - # Batch fetch views data for all posts - post_ids = [p.post.id for p in placements if p.post] - views_map = await self.database.get_latest_views_data_batch(post_ids) if post_ids else {} - - # Build output DTOs placement_outputs = [] for placement in placements: - # Compute post-related fields ad_post_url = None - views_count = None - views_availability = domain.PostViewsAvailability.UNKNOWN - last_views_fetch_at = None if placement.post: ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id) - views_data = views_map.get(placement.post.id) - if views_data: - views_count, last_views_fetch_at = views_data - views_availability = domain.PostViewsAvailability.AVAILABLE placement_outputs.append( dto.PlacementOutput( @@ -65,9 +52,6 @@ async def get_placements(self: 'Usecase', input: dto.GetPlacementsInput) -> dto. invite_link=placement.invite_link, status=placement.status, subscriptions_count=placement.subscriptions_count, - views_count=views_count, - views_availability=views_availability, - last_views_fetch_at=last_views_fetch_at, created_at=placement.created_at, ) ) diff --git a/src/usecase/placement/update_placement.py b/src/usecase/placement/update_placement.py index 2214722..93f45a5 100644 --- a/src/usecase/placement/update_placement.py +++ b/src/usecase/placement/update_placement.py @@ -39,21 +39,11 @@ async def update_placement( await self.database.update_placement(placement) - # Compute post-related fields ad_post_url = None - views_count = None - views_availability = domain.PostViewsAvailability.UNKNOWN - last_views_fetch_at = None if placement.post: ad_post_url = generate_post_url(placement.post.channel.username, placement.post.message_id) - # Get latest views data - views_data = await self.database.get_latest_views_data(placement.post.id) - if views_data: - views_count, last_views_fetch_at = views_data - views_availability = domain.PostViewsAvailability.AVAILABLE - return dto.PlacementOutput( id=placement.id, project_id=placement.project_id, @@ -70,8 +60,5 @@ async def update_placement( invite_link=placement.invite_link, status=placement.status, subscriptions_count=placement.subscriptions_count, - views_count=views_count, - views_availability=views_availability, - last_views_fetch_at=last_views_fetch_at, created_at=placement.created_at, ) diff --git a/src/usecase/purchase_plan/attach_channel_to_purchase_plan.py b/src/usecase/purchase_plan/attach_channel_to_purchase_plan.py index ea17133..0e96b87 100644 --- a/src/usecase/purchase_plan/attach_channel_to_purchase_plan.py +++ b/src/usecase/purchase_plan/attach_channel_to_purchase_plan.py @@ -27,16 +27,20 @@ async def attach_channel_to_purchase_plan( plan = await self.ensure_active_purchase_plan(project) - # Поиск канала по username channel = await self.database.get_channel(username=input.username) if not channel: - # Создать канал с nullable telegram_id/title (заполнятся парсером позже) - channel = domain.Channel(username=input.username) + parser_response = await self.parser_client.fetch_telegram_channel(input.username) + if not parser_response: + raise domain.TelegramChannelNotFound(input.username) + + channel = domain.Channel( + username=input.username, + telegram_id=parser_response.telegram_id, + title=parser_response.title, + ) await self.database.create_channel(channel) - log.info('Created channel with username @%s (pending parser update)', input.username) - else: - log.info('Found existing channel %s for username @%s', channel.id, input.username) + log.info('Created channel @%s with telegram_id=%s', input.username, parser_response.telegram_id) plan_channel = await self.database.add_channel_to_purchase_plan( plan.id,