feat: creatives and external channels

This commit is contained in:
Artem Tsyrulnikov
2025-11-10 15:13:38 +03:00
parent 3800c72662
commit cd167fdb43
73 changed files with 3024 additions and 947 deletions

210
src/adapter/postgres.py Normal file
View File

@@ -0,0 +1,210 @@
import datetime
import uuid
from sqlalchemy import delete, select
from sqlalchemy.orm import selectinload
from shared.datebase_base import DatabaseBase
from src import domain
class Postgres(DatabaseBase):
async def get_user(self, *, user_id: uuid.UUID | None = None, telegram_id: int | None = None) -> domain.User | None:
if user_id:
return await self.session.get(domain.User, user_id)
elif telegram_id:
stmt = select(domain.User).where(domain.User.telegram_id == telegram_id)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
raise ValueError('Either user_id or telegram_id must be provided')
async def create_user(self, user: domain.User) -> domain.User:
self.session.add(user)
await self.session.flush()
await self.session.refresh(user)
return user
async def create_login_token(
self, user_id: uuid.UUID, token: str, expires_at: datetime.datetime
) -> domain.LoginToken:
login_token = domain.LoginToken(user_id=user_id, token=token, expires_at=expires_at)
self.session.add(login_token)
await self.session.flush()
await self.session.refresh(login_token)
return login_token
async def get_login_token(self, token: str) -> domain.LoginToken | None:
stmt = select(domain.LoginToken).where(domain.LoginToken.token == token)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def mark_token_as_used(self, token: str) -> None:
stmt = select(domain.LoginToken).where(domain.LoginToken.token == token)
result = await self.session.execute(stmt)
login_token = result.scalar_one_or_none()
if login_token:
login_token.used_at = datetime.datetime.now(datetime.UTC)
await self.session.flush()
async def get_target_channel(
self, user_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
) -> domain.TargetChannel | None:
if not channel_id and not telegram_id:
raise ValueError('Either channel_id or telegram_id must be provided')
stmt = select(domain.TargetChannel).where(domain.TargetChannel.user_id == user_id)
if channel_id:
stmt = stmt.where(domain.TargetChannel.id == channel_id)
if telegram_id:
stmt = stmt.where(domain.TargetChannel.telegram_id == telegram_id)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create_target_channel(self, channel: domain.TargetChannel) -> domain.TargetChannel:
self.session.add(channel)
await self.session.flush()
await self.session.refresh(channel)
return channel
async def upsert_target_channel(self, channel: domain.TargetChannel) -> domain.TargetChannel:
existing = await self.get_target_channel(user_id=channel.user_id, telegram_id=channel.telegram_id)
if not existing:
self.session.add(channel)
await self.session.flush()
await self.session.refresh(channel)
return channel
existing.title = channel.title
existing.username = channel.username
existing.user_id = channel.user_id
existing.is_active = channel.is_active
existing.deleted_at = None
existing.updated_at = datetime.datetime.now(datetime.UTC)
await self.session.flush()
await self.session.refresh(existing)
return existing
async def get_user_target_channels(self, user_id: uuid.UUID) -> list[domain.TargetChannel]:
stmt = select(domain.TargetChannel).where(
domain.TargetChannel.status == domain.TargetChannelStatus.ACTIVE,
domain.TargetChannel.user_id == user_id,
)
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def update_target_channel(self, channel: domain.TargetChannel) -> domain.TargetChannel:
await self.session.flush()
await self.session.refresh(channel)
return channel
async def get_external_channel(
self, user_id: uuid.UUID, channel_id: uuid.UUID | None = None, telegram_id: int | None = None
) -> domain.ExternalChannel | None:
stmt = select(domain.ExternalChannel).where(domain.ExternalChannel.user_id == user_id)
if channel_id:
stmt = stmt.where(domain.ExternalChannel.id == channel_id)
if telegram_id:
stmt = stmt.where(domain.ExternalChannel.telegram_id == telegram_id)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create_external_channel(self, channel: domain.ExternalChannel) -> domain.ExternalChannel:
self.session.add(channel)
await self.session.flush()
await self.session.refresh(channel)
return channel
async def get_external_channels_for_target(self, target_channel_id: uuid.UUID) -> list[domain.ExternalChannel]:
stmt = (
select(domain.ExternalChannel)
.join(domain.ExternalChannel.target_channels)
.where(domain.TargetChannel.id == target_channel_id)
)
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def add_external_channel_to_targets(
self, external_channel_id: uuid.UUID, target_channel_ids: list[uuid.UUID]
) -> None:
external_channel = await self.session.get(domain.ExternalChannel, external_channel_id)
if not external_channel:
raise ValueError(f'ExternalChannel {external_channel_id} not found')
for target_id in target_channel_ids:
target_channel = await self.session.get(domain.TargetChannel, target_id)
if target_channel and target_channel not in external_channel.target_channels:
external_channel.target_channels.append(target_channel)
await self.session.flush()
async def remove_external_channel_from_target(
self, external_channel_id: uuid.UUID, target_channel_id: uuid.UUID
) -> None:
external_channel = await self.session.get(domain.ExternalChannel, external_channel_id)
if not external_channel:
raise ValueError(f'ExternalChannel {external_channel_id} not found')
target_channel = await self.session.get(domain.TargetChannel, target_channel_id)
if target_channel and target_channel in external_channel.target_channels:
external_channel.target_channels.remove(target_channel)
await self.session.flush()
async def delete_external_channel(self, channel_id: uuid.UUID) -> None:
stmt = delete(domain.ExternalChannel).where(domain.ExternalChannel.id == channel_id)
await self.session.execute(stmt)
await self.session.flush()
async def get_creative(self, creative_id: uuid.UUID, user_id: uuid.UUID) -> domain.Creative | None:
stmt = (
select(domain.Creative)
.options(selectinload(domain.Creative.target_channel))
.where(domain.Creative.id == creative_id, domain.Creative.user_id == user_id)
)
result = await self.session.execute(stmt)
return result.scalar_one_or_none()
async def create_creative(self, creative: domain.Creative) -> domain.Creative:
self.session.add(creative)
await self.session.flush()
await self.session.refresh(creative)
return creative
async def update_creative(self, creative: domain.Creative) -> domain.Creative:
await self.session.flush()
await self.session.refresh(creative)
return creative
async def get_user_creatives(
self, user_id: uuid.UUID, *, target_channel_id: uuid.UUID | None = None, include_archived: bool = False
) -> list[domain.Creative]:
stmt = (
select(domain.Creative)
.options(selectinload(domain.Creative.target_channel))
.where(domain.Creative.user_id == user_id)
)
if target_channel_id:
stmt = stmt.where(domain.Creative.target_channel_id == target_channel_id)
if not include_archived:
stmt = stmt.where(domain.Creative.status == domain.CreativeStatus.ACTIVE)
stmt = stmt.order_by(domain.Creative.created_at.desc())
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def delete_creative(self, creative_id: uuid.UUID) -> None:
stmt = delete(domain.Creative).where(domain.Creative.id == creative_id)
await self.session.execute(stmt)
await self.session.flush()