feat: subs and views

This commit is contained in:
Artem Tsyrulnikov
2025-11-10 17:08:56 +03:00
parent 2549242b06
commit 0bed5c266d
34 changed files with 1706 additions and 119 deletions

View File

@@ -5,6 +5,7 @@ from src.controller.http.creatives import creatives_router
from src.controller.http.external_channels import external_channels_router
from src.controller.http.purchases import purchases_router
from src.controller.http.target_channels import target_channels_router
from src.controller.http.views import views_router
api_router = APIRouter()
@@ -17,5 +18,6 @@ api_v1_router.include_router(target_channels_router)
api_v1_router.include_router(external_channels_router)
api_v1_router.include_router(creatives_router)
api_v1_router.include_router(purchases_router)
api_v1_router.include_router(views_router)
api_router.include_router(api_v1_router)

View File

@@ -0,0 +1,59 @@
import datetime
import uuid
from typing import Annotated
from fastapi import Depends
from fastapi.routing import APIRouter
from src import dependencies, dto
from src.adapter.jwt import JWTPayload
views_router = APIRouter(prefix='/purchases/{purchase_id}/views', tags=['views'])
@views_router.get('/history')
async def get_views_history(
purchase_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
from_date: datetime.datetime | None = None,
to_date: datetime.datetime | None = None,
) -> dto.GetViewsHistoryOutput:
"""Получить историю просмотров для закупа."""
input_data = dto.GetViewsHistoryInput(
purchase_id=purchase_id,
user_id=current_user.user_id,
from_date=from_date,
to_date=to_date,
)
return await dependencies.get_usecase().get_views_history(input=input_data)
@views_router.post('/fetch')
async def fetch_views(
purchase_id: uuid.UUID,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.FetchViewsOutput:
"""Получить актуальные просмотры через Telegram API."""
input_data = dto.FetchViewsInput(
purchase_id=purchase_id,
user_id=current_user.user_id,
)
return await dependencies.get_usecase().fetch_views(input=input_data)
@views_router.post('/manual')
async def update_views_manually(
purchase_id: uuid.UUID,
views_count: int,
current_user: Annotated[JWTPayload, Depends(dependencies.get_current_user)],
) -> dto.PurchaseOutput:
"""Обновить просмотры вручную."""
input_data = dto.UpdateViewsManuallyInput(
purchase_id=purchase_id,
user_id=current_user.user_id,
views_count=views_count,
)
return await dependencies.get_usecase().update_views_manually(input=input_data)

View File

@@ -3,4 +3,4 @@ from aiogram import Router
telegram_callback_router = Router()
# Import handlers to register them
from . import my_chat_member, start_with_login # noqa: E402, F401
from . import chat_join_request, chat_member_updated, my_chat_member, start_with_login # noqa: E402, F401

View File

@@ -0,0 +1,37 @@
import logging
from aiogram.types import ChatJoinRequest
from src import dependencies
from src.controller.telegram_callback import telegram_callback_router
log = logging.getLogger(__name__)
@telegram_callback_router.chat_join_request()
async def on_chat_join_request(event: ChatJoinRequest) -> None:
"""
Обрабатывает запросы на вступление в канал через приватную invite_link с одобрением.
Событие приходит когда пользователь отправляет запрос на вступление в канал.
"""
if event.chat.type not in {'channel', 'supergroup'}:
return
if not event.from_user:
log.error('Failed to get user data from chat join request')
return
# Получаем invite_link, по которой пользователь отправил запрос
invite_link = event.invite_link
if not invite_link or not invite_link.invite_link:
log.debug('No invite_link in chat join request event')
return
usecase = dependencies.get_usecase()
await usecase.handle_subscription(
user_telegram_id=event.from_user.id,
username=event.from_user.username,
invite_link=invite_link.invite_link,
)

View File

@@ -0,0 +1,48 @@
import logging
from aiogram.types import ChatMemberUpdated
from src import dependencies
from src.controller.telegram_callback import telegram_callback_router
log = logging.getLogger(__name__)
@telegram_callback_router.chat_member()
async def on_chat_member_updated(event: ChatMemberUpdated) -> None:
"""
Обрабатывает события подписки пользователей в канал через открытую invite_link.
Событие приходит когда пользователь вступает в канал напрямую (без запроса на вступление).
"""
if event.chat.type not in {'channel', 'supergroup'}:
return
if not event.from_user:
log.error('Failed to get user data from chat member update')
return
old_status = event.old_chat_member.status
new_status = event.new_chat_member.status
# Проверяем что пользователь присоединился к каналу
was_not_member = old_status in ('left', 'kicked')
is_now_member = new_status in ('member', 'administrator', 'creator')
user_joined = was_not_member and is_now_member
if not user_joined:
return
# Получаем invite_link, по которой пользователь присоединился
invite_link = event.invite_link
if not invite_link or not invite_link.invite_link:
log.debug('No invite_link in chat member update event')
return
usecase = dependencies.get_usecase()
await usecase.handle_subscription(
user_telegram_id=event.from_user.id,
username=event.from_user.username,
invite_link=invite_link.invite_link,
)