diff --git a/src/domain/creative.py b/src/domain/creative.py
index 5d7e5d4..061b760 100644
--- a/src/domain/creative.py
+++ b/src/domain/creative.py
@@ -41,15 +41,16 @@ MAX_CREATIVE_MEDIA_BYTES = 20 * 1024 * 1024
_INVITE_LINK_HTML = re.compile(
- r'([^<]*)',
- re.IGNORECASE,
+ r'(.*?)',
+ re.IGNORECASE | re.DOTALL,
)
_INVITE_LINK_PLAIN = re.compile(r'https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+', re.IGNORECASE)
-_INVITE_LINK_REPLACED = re.compile(r'([^<]*)', re.IGNORECASE)
-_INVITE_LINK_REPLACED_URL = re.compile(
- r'\s*https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+\s*',
- re.IGNORECASE,
-)
+_INVITE_LINK_REPLACED = re.compile(r'(.*?)', re.IGNORECASE | re.DOTALL)
+_HTML_TAG = re.compile(r'<[^>]+>')
+
+
+def _strip_html(text: str) -> str:
+ return _HTML_TAG.sub('', text).strip()
def replace_invite_link_with_tag(text: str) -> str:
@@ -70,14 +71,20 @@ def replace_invite_link_with_tag(text: str) -> str:
if total > 1:
raise CreativeMultipleInviteLinks()
- # Если ссылка уже заменена, нормализуем текст если внутри был URL
+ # Если ссылка уже заменена, нормализуем текст если внутри был URL или теги
if replaced_count == 1:
- return _INVITE_LINK_REPLACED_URL.sub('', text)
+ def _normalize_replaced(match: re.Match[str]) -> str:
+ inner = _strip_html(match.group(1))
+ if inner == "" or _INVITE_LINK_PLAIN.search(inner):
+ return ''
+ return f'{inner}'
+
+ return _INVITE_LINK_REPLACED.sub(_normalize_replaced, text)
if html_count == 1:
def _replace_html(match: re.Match[str]) -> str:
- inner = match.group(1).strip()
+ inner = _strip_html(match.group(1))
if _INVITE_LINK_PLAIN.fullmatch(inner):
return ''
return f'{inner}'
diff --git a/src/domain/post.py b/src/domain/post.py
index 6ff74dd..c867d0c 100644
--- a/src/domain/post.py
+++ b/src/domain/post.py
@@ -27,6 +27,15 @@ class Post(TimestampedModel):
@property
def url(self) -> str | None:
- if not self.channel.username:
+ if self.channel.username:
+ return f'https://t.me/{self.channel.username}/{self.message_id}'
+
+ telegram_id = self.channel.telegram_id
+ if telegram_id is None:
return None
- return f'https://t.me/{self.channel.username}/{self.message_id}'
+
+ channel_id = telegram_id
+ if telegram_id < 0:
+ channel_id = -telegram_id - 1000000000000
+
+ return f'https://t.me/c/{channel_id}/{self.message_id}'
diff --git a/tg_bot/bot/bot.go b/tg_bot/bot/bot.go
index ef0fbb5..7831e66 100644
--- a/tg_bot/bot/bot.go
+++ b/tg_bot/bot/bot.go
@@ -321,7 +321,11 @@ func (b *Bot) Update(u *echotron.Update) {
b.exec = e
defer func() { b.exec = nil }()
- if b.CurrentState == nil {
+ isStartCommand := u.Message != nil && (u.Message.Text == "/start" ||
+ u.Message.Text == "/start login" ||
+ strings.HasPrefix(u.Message.Text, "/start project_"))
+
+ if b.CurrentState == nil && !isStartCommand {
b.SetState(b.commandRouter("/start"), EditMessage)
}
diff --git a/tg_bot/screens/creative_editor.go b/tg_bot/screens/creative_editor.go
index 2c5a749..79cffca 100644
--- a/tg_bot/screens/creative_editor.go
+++ b/tg_bot/screens/creative_editor.go
@@ -2,6 +2,7 @@ package screens
import (
"fmt"
+ "regexp"
"strings"
"github.com/NicoNex/echotron/v3"
@@ -49,6 +50,10 @@ const inviteLinkPlaceholder = "{{invite_link}}"
const inviteLinkPreviewURL = "https://t.me/+nlOUmIsLGAlmZTMy" //ссылка приглашения для предпросмотра
const buttonURLPlaceholder = "https://example.com"
+var inviteLinkPattern = regexp.MustCompile(`https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+`)
+var inviteLinkTagPattern = regexp.MustCompile(`\s*(.*?)\s*`)
+var htmlTagPattern = regexp.MustCompile(`<[^>]+>`)
+
const (
inputModeText = "text"
inputModeButtonText = "button_text"
@@ -90,55 +95,23 @@ func (e *CreativeEditorFields) GetCreativeText() string {
return msgCreativeDefault
}
- // Заменяем пригласительные ссылки с классом tg-link на preview-ссылку
- // Находим все вхождения ... и заменяем на кликабельную ссылку
- for strings.Contains(text, ``) {
- startIdx := strings.Index(text, ``)
- if startIdx == -1 {
- break
+ // Заменяем плейсхолдеры tg-link на кликабельную preview-ссылку.
+ text = inviteLinkTagPattern.ReplaceAllStringFunc(text, func(match string) string {
+ sub := inviteLinkTagPattern.FindStringSubmatch(match)
+ inner := ""
+ if len(sub) > 1 {
+ inner = strings.TrimSpace(htmlTagPattern.ReplaceAllString(sub[1], ""))
}
- endIdx := strings.Index(text[startIdx:], "")
- if endIdx == -1 {
- log.Warn().Msg("Found but no closing ")
- break
+ if inner == "" {
+ inner = inviteLinkPreviewURL
}
- endIdx += startIdx
+ return fmt.Sprintf(`%s`, inviteLinkPreviewURL, inner)
+ })
- // Извлекаем текст между тегами
- linkText := text[startIdx+len(``) : endIdx]
- // ВАЖНО: Удаляем все HTML-теги из текста ссылки для предотвращения неправильной вложенности
- // Это нужно для старых креативов с неправильным HTML
- for {
- openTag := strings.Index(linkText, "<")
- if openTag == -1 {
- break
- }
- closeTag := strings.Index(linkText[openTag:], ">")
- if closeTag == -1 {
- log.Warn().Str("linkText", linkText).Msg("Found < but no closing >")
- break
- }
- // Удаляем тег
- linkText = linkText[:openTag] + linkText[openTag+closeTag+1:]
- }
+ // Заменяем пригласительные ссылки на preview URL только для отображения.
+ text = inviteLinkPattern.ReplaceAllString(text, inviteLinkPreviewURL)
- linkText = strings.TrimSpace(linkText)
-
- // Если текст ссылки не пустой, добавляем атрибут href с примером ссылки
- // Если пустой, показываем просто текст примера ссылки (без тега)
- var replacement string
- if linkText != "" {
- // Есть текст - добавляем href атрибут к тегу
- replacement = fmt.Sprintf(`%s`, inviteLinkPreviewURL, linkText)
- } else {
- // Пусто - показываем просто текст примера ссылки
- replacement = fmt.Sprintf(`%s`, inviteLinkPreviewURL)
- }
-
- text = text[:startIdx] + replacement + text[endIdx+len(""):]
- }
-
- // Также заменяем плейсхолдер {{invite_link}} на preview URL (как в кнопках)
+ // Также заменяем плейсхолдер {{invite_link}} на preview URL (как в кнопках).
text = strings.ReplaceAll(text, inviteLinkPlaceholder, inviteLinkPreviewURL)
return text
diff --git a/tg_bot/screens/ui/message_format.go b/tg_bot/screens/ui/message_format.go
index 123847f..17003f1 100644
--- a/tg_bot/screens/ui/message_format.go
+++ b/tg_bot/screens/ui/message_format.go
@@ -2,7 +2,6 @@ package ui
import (
"fmt"
- "regexp"
"sort"
"strings"
@@ -24,7 +23,6 @@ var simpleHTMLTags = map[echotron.MessageEntityType]htmlTag{
}
// FormatMessageHTML converts Telegram entities to HTML and escapes the rest.
-// Invite links are normalized into placeholders for previews.
func FormatMessageHTML(message *echotron.Message) string {
if message == nil {
return ""
@@ -138,22 +136,12 @@ func FormatMessageHTML(message *echotron.Message) string {
openTag, closeTag = tag.open, tag.close
} else if entity.Type == echotron.UrlEntity {
entityText := string(runes[start:end])
- if isInviteLink(entityText) {
- openTag = ``
- closeTag = ""
- } else {
- openTag = fmt.Sprintf("", entityText)
- closeTag = ""
- }
+ openTag = fmt.Sprintf("", entityText)
+ closeTag = ""
} else if entity.Type == echotron.TextLinkEntity {
if entity.URL != "" {
- if isInviteLink(entity.URL) {
- openTag = ``
- closeTag = ""
- } else {
- openTag = fmt.Sprintf("", entity.URL)
- closeTag = ""
- }
+ openTag = fmt.Sprintf("", entity.URL)
+ closeTag = ""
}
}
@@ -204,12 +192,7 @@ func FormatMessageHTML(message *echotron.Message) string {
}
}
- return normalizeInviteLinkTags(normalizeHTMLTags(result.String()))
-}
-
-// isInviteLink detects Telegram invite links used in creatives.
-func isInviteLink(url string) bool {
- return strings.Contains(url, "t.me/+") || strings.Contains(url, "t.me/joinchat/")
+ return normalizeHTMLTags(result.String())
}
func normalizeHTMLTags(input string) string {
@@ -291,15 +274,6 @@ func normalizeHTMLTags(input string) string {
return out.String()
}
-func normalizeInviteLinkTags(input string) string {
- if input == "" {
- return input
- }
-
- re := regexp.MustCompile(`\s*https?://t\.me/(?:\+|joinchat/)[a-zA-Z0-9_-]+\s*`)
- return re.ReplaceAllString(input, ``)
-}
-
// EscapeHTML escapes HTML special characters for safe output.
func EscapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&")
diff --git a/tg_parser/internal/adapter/database/get_tracked_posts.go b/tg_parser/internal/adapter/database/get_tracked_posts.go
index b3b0234..f78fd0d 100644
--- a/tg_parser/internal/adapter/database/get_tracked_posts.go
+++ b/tg_parser/internal/adapter/database/get_tracked_posts.go
@@ -18,6 +18,7 @@ func (d *Database) GetTrackedPosts(ctx context.Context, channel domain.Channel)
p.text,
CASE
WHEN c.username IS NOT NULL THEN CONCAT('https://t.me/', c.username, '/', p.message_id)
+ WHEN c.telegram_id IS NOT NULL THEN CONCAT('https://t.me/c/', (-c.telegram_id - 1000000000000), '/', p.message_id)
ELSE ''
END as link,
COALESCE(
diff --git a/tg_parser/internal/domain/post.go b/tg_parser/internal/domain/post.go
index e7eea11..8e6f90d 100644
--- a/tg_parser/internal/domain/post.go
+++ b/tg_parser/internal/domain/post.go
@@ -19,6 +19,8 @@ func NewPost(channel Channel, messageID int, text string, views int) Post {
link := ""
if channel.Username != "" {
link = fmt.Sprintf("https://t.me/%s/%d", channel.Username, messageID)
+ } else if channel.TelegramID != 0 {
+ link = fmt.Sprintf("https://t.me/c/%d/%d", ChannelIDFromChatID(channel.TelegramID), messageID)
}
return Post{
ID: uuid.New(),