This commit is contained in:
Artem Tsyrulnikov
2026-01-19 12:50:36 +03:00
parent 62e8f98429
commit 5473b26787
17 changed files with 478 additions and 599 deletions

View File

@@ -383,7 +383,14 @@ class Postgres(DatabaseBase):
query = domain.Placement.filter(project__workspace_id=workspace_id, project_id=project_id)
if not include_archived:
query = query.filter(status__in=[domain.PlacementStatus.APPROVED, domain.PlacementStatus.IN_PROGRESS])
query = query.filter(
status__in=[
domain.PlacementStatus.PLANNED,
domain.PlacementStatus.APPROVED,
domain.PlacementStatus.IN_PROGRESS,
domain.PlacementStatus.COMPLETED,
]
)
return (
await query.prefetch_related('project', 'project__channel', 'channel', 'creative')

View File

@@ -40,22 +40,25 @@ class Creative(TimestampedModel):
MAX_CREATIVE_MEDIA_BYTES = 20 * 1024 * 1024
_INVITE_LINK_HTML = re.compile(r'<a\s+href=["\']https?://t\.me/\+[a-zA-Z0-9_-]+["\']>([^<]*)</a>', re.IGNORECASE)
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/\+[a-zA-Z0-9_-]+', re.IGNORECASE)
_INVITE_LINK_HTML = re.compile(r'<a\s+href=["\']https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+["\']>([^<]*)</a>', re.IGNORECASE)
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+', re.IGNORECASE)
_INVITE_LINK_REPLACED = re.compile(r'<a\s+class=["\']tg-link["\']>([^<]*)</a>', re.IGNORECASE)
_INVITE_LINK_REPLACED_URL = re.compile(
r'<a\s+class=["\']tg-link["\']>\s*https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+\s*</a>',
re.IGNORECASE,
)
def replace_invite_link_with_tag(text: str) -> str:
"""Replace Telegram invite link (t.me/+xxx) with tracking tag."""
# Проверяем, есть ли уже замененная ссылка
replaced_count = len(_INVITE_LINK_REPLACED.findall(text))
html_count = len(_INVITE_LINK_HTML.findall(text))
# Сначала удаляем HTML-ссылки из текста, чтобы не считать их дважды
text_without_html_links = _INVITE_LINK_HTML.sub('', text)
plain_count = len(_INVITE_LINK_PLAIN.findall(text_without_html_links))
# Сначала удаляем HTML-ссылки и tg-link теги, чтобы не считать их дважды
text_without_links = _INVITE_LINK_HTML.sub('', text)
text_without_links = _INVITE_LINK_REPLACED.sub('', text_without_links)
plain_count = len(_INVITE_LINK_PLAIN.findall(text_without_links))
total = replaced_count + html_count + plain_count
@@ -64,12 +67,19 @@ def replace_invite_link_with_tag(text: str) -> str:
if total > 1:
raise CreativeMultipleInviteLinks()
# Если ссылка уже заменена, возвращаем текст как есть
# Если ссылка уже заменена, нормализуем текст если внутри был URL
if replaced_count == 1:
return text
return _INVITE_LINK_REPLACED_URL.sub('<a class="tg-link"></a>', text)
if html_count == 1:
return _INVITE_LINK_HTML.sub(r'<a class="tg-link">\1</a>', text)
def _replace_html(match: re.Match) -> str:
inner = match.group(1).strip()
if _INVITE_LINK_PLAIN.fullmatch(inner):
return '<a class="tg-link"></a>'
return f'<a class="tg-link">{inner}</a>'
return _INVITE_LINK_HTML.sub(_replace_html, text)
return _INVITE_LINK_PLAIN.sub('<a class="tg-link"></a>', text)

View File

@@ -1,7 +1,7 @@
import logging
from typing import TYPE_CHECKING
from src import dto
from src import domain, dto
if TYPE_CHECKING:
from .. import Usecase
@@ -12,6 +12,44 @@ log = logging.getLogger(__name__)
async def get_channels(self: 'Usecase', input: dto.GetChannelsInput) -> list[dto.ChannelOutput]:
channels = await self.database.search_channels(username_query=input.username)
if not channels and input.username:
try:
parser_response = await self.parser.fetch_telegram_channel(input.username)
except Exception as exc:
log.warning('Parser fetch failed for username query %s: %s', input.username, exc)
parser_response = None
if parser_response:
channel = await self.database.get_channel(telegram_id=parser_response.telegram_id)
if not channel:
channel = await self.database.get_channel(username=parser_response.username)
if channel:
updated = False
if channel.username != parser_response.username:
channel.username = parser_response.username
updated = True
if channel.title != parser_response.title:
channel.title = parser_response.title
updated = True
if channel.telegram_id != parser_response.telegram_id:
channel.telegram_id = parser_response.telegram_id
updated = True
if updated:
await self.database.update_channel(channel)
log.info('Updated channel @%s from parser response', channel.username)
else:
channel = domain.Channel(
username=parser_response.username,
telegram_id=parser_response.telegram_id,
title=parser_response.title,
)
await self.database.create_channel(channel)
log.info('Created channel @%s from parser response', channel.username)
channels = [channel]
log.debug('Found %s channels for username query: %s', len(channels), input.username)
return [

View File

@@ -46,7 +46,12 @@ async def fetch_placement_post_cycle(self: 'Usecase', interval_seconds: int) ->
placement_post = domain.PlacementPost(
placement_id=placement.id,
project_id=placement.project_id,
creative_id=placement.creative_id,
post_id=post.id,
wanted_placement_date=placement.placement_at or post.created_at,
cost=placement.cost_value,
comment=placement.comment,
status=domain.PlacementPostStatus.ACTIVE,
)