фиксы

This commit is contained in:
Artem Tsyrulnikov
2026-01-08 21:50:04 +03:00
parent 3e5bdb8164
commit eb54b01835
20 changed files with 462 additions and 405 deletions

View File

@@ -45,7 +45,7 @@ type CreativeEditorFields struct {
}
const inviteLinkPlaceholder = "{{invite_link}}"
const inviteLinkPreviewURL = "https://t.me/J7_channel"
const inviteLinkPreviewURL = "https://t.me/+nlOUmIsLGAlmZTMy" //ссылка приглашения для предпросмотра
// GetCreativeText возвращает текст для превью креатива
func (e *CreativeEditorFields) GetCreativeText() string {
@@ -64,8 +64,79 @@ func (e *CreativeEditorFields) GetCreativeText() string {
</i>`
}
// Заменяем пустую ссылку <a class="tg-link"></a> на валидную preview-ссылку
text = strings.ReplaceAll(text, `<a class="tg-link"></a>`, `<a href="`+inviteLinkPreviewURL+`">👉 Подписаться</a>`)
// Заменяем пригласительные ссылки с классом tg-link на preview-ссылку
// Находим все вхождения <a class="tg-link">...</a> и заменяем на кликабельную ссылку
replacementCount := 0
for strings.Contains(text, `<a class="tg-link">`) {
startIdx := strings.Index(text, `<a class="tg-link">`)
if startIdx == -1 {
break
}
endIdx := strings.Index(text[startIdx:], "</a>")
if endIdx == -1 {
log.Warn().Msg("Found <a class=\"tg-link\"> but no closing </a>")
break
}
endIdx += startIdx
// Извлекаем текст между тегами
linkText := text[startIdx+len(`<a class="tg-link">`) : endIdx]
log.Info().
Int("replacement_num", replacementCount).
Str("linkText_before_sanitize", linkText).
Msg("Processing tg-link")
// ВАЖНО: Удаляем все HTML-теги из текста ссылки для предотвращения неправильной вложенности
// Это нужно для старых креативов с неправильным HTML
tagsRemoved := 0
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
}
// Удаляем тег
removedTag := linkText[openTag : openTag+closeTag+1]
linkText = linkText[:openTag] + linkText[openTag+closeTag+1:]
tagsRemoved++
log.Info().
Str("removed_tag", removedTag).
Str("linkText_after", linkText).
Int("tags_removed_total", tagsRemoved).
Msg("Removed HTML tag from link text")
}
linkText = strings.TrimSpace(linkText)
// Если текст ссылки не пустой, добавляем атрибут href с примером ссылки
// Если пустой, показываем просто текст примера ссылки (без тега)
var replacement string
if linkText != "" {
// Есть текст - добавляем href атрибут к тегу
replacement = fmt.Sprintf(`<a class="tg-link" href="%s">%s</a>`, inviteLinkPreviewURL, linkText)
} else {
// Пусто - показываем просто текст примера ссылки
replacement = fmt.Sprintf(`%s`, inviteLinkPreviewURL)
}
log.Info().
Str("linkText_after_sanitize", linkText).
Int("tags_removed", tagsRemoved).
Str("replacement", replacement).
Msg("Sanitized link text")
text = text[:startIdx] + replacement + text[endIdx+len("</a>"):]
replacementCount++
}
log.Info().
Int("replacements_made", replacementCount).
Str("final_text", text).
Msg("Finished processing tg-links")
// Также заменяем плейсхолдер {{invite_link}} на preview URL (как в кнопках)
text = strings.ReplaceAll(text, inviteLinkPlaceholder, inviteLinkPreviewURL)
@@ -409,18 +480,55 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
// Преобразуем текст в руны для правильной работы с UTF-8
runes := []rune(text)
// Сначала находим все url entities, чтобы исключить перекрывающиеся форматирования
urlRanges := make([][2]int, 0)
// Также отмечаем диапазоны, которые нужно полностью исключить из текста (простые url ссылки)
skipRanges := make(map[[2]int]bool)
for _, entity := range entities {
if entity.Type == "url" || entity.Type == "text_link" {
urlRanges = append(urlRanges, [2]int{entity.Offset, entity.Offset + entity.Length})
// Для простых url (не text_link) с пригласительными ссылками - пропускаем текст URL
// Создаем пустой <a class="tg-link"></a>, а сам URL не сохраняем
if entity.Type == "url" {
entityText := ""
if entity.Offset+entity.Length <= len(runes) {
entityText = string(runes[entity.Offset : entity.Offset+entity.Length])
}
if strings.Contains(entityText, "t.me/+") || strings.Contains(entityText, "t.me/joinchat/") {
skipRanges[[2]int{entity.Offset, entity.Offset + entity.Length}] = true
}
}
}
}
// Проверяем, находится ли entity внутри URL
isInsideURL := func(offset, length int) bool {
for _, urlRange := range urlRanges {
// Если entity полностью внутри URL (но не сам URL)
if offset > urlRange[0] && offset+length <= urlRange[1] {
return true
}
// Если entity пересекается с URL
if offset < urlRange[1] && offset+length > urlRange[0] && (offset != urlRange[0] || length != urlRange[1]-urlRange[0]) {
return true
}
}
return false
}
var spans []tagSpan
for _, entity := range entities {
// URL уже присутствует в сыром тексте, чтобы не дублировать - пропускаем entity.
if entity.Type == "url" {
continue
}
entityText := ""
if entity.Offset+entity.Length <= len(runes) {
entityText = string(runes[entity.Offset : entity.Offset+entity.Length])
}
// Пропускаем форматирование (bold, italic, etc), если оно находится внутри URL
if entity.Type != "url" && entity.Type != "text_link" && isInsideURL(entity.Offset, entity.Length) {
continue
}
var openTag, closeTag string
switch entity.Type {
case "bold":
@@ -435,10 +543,29 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
openTag, closeTag = "<code>", "</code>"
case "pre":
openTag, closeTag = "<pre>", "</pre>"
case "url":
// Конвертируем простые URL в HTML-ссылки
// Для пригласительных ссылок (t.me/+ или t.me/joinchat/) используем пустой тег
if strings.Contains(entityText, "t.me/+") || strings.Contains(entityText, "t.me/joinchat/") {
// Пригласительная ссылка - создаем пустой <a class="tg-link"></a>
// Текст ссылки будет пропущен при рендеринге (см. skipRanges)
openTag = `<a class="tg-link">`
closeTag = "</a>"
} else {
// Обычная ссылка - оставляем как есть (ссылка на себя)
openTag = fmt.Sprintf("<a href=\"%s\">", entityText)
closeTag = "</a>"
}
case "text_link":
if entity.URL != "" {
openTag = fmt.Sprintf("<a href=\"%s\">", entity.URL)
closeTag = "</a>"
// Проверяем, является ли URL пригласительной ссылкой
if strings.Contains(entity.URL, "t.me/+") || strings.Contains(entity.URL, "t.me/joinchat/") {
openTag = `<a class="tg-link">`
closeTag = "</a>"
} else {
openTag = fmt.Sprintf("<a href=\"%s\">", entity.URL)
closeTag = "</a>"
}
}
default:
_ = entityText
@@ -462,6 +589,16 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
closes[span.end] = append(closes[span.end], span)
}
// Функция для проверки, находится ли позиция в skipRanges
isInSkipRange := func(pos int) bool {
for skipRange := range skipRanges {
if pos >= skipRange[0] && pos < skipRange[1] {
return true
}
}
return false
}
// Пишем текст один раз, вставляя HTML-теги по позициям
// и экранируя HTML-спецсимволы
var result strings.Builder
@@ -479,6 +616,10 @@ func (e *CreativeEditorFields) GetFormattedText(message *echotron.Message) strin
}
}
if i < len(runes) {
// Пропускаем символы, которые находятся в skipRanges (текст пригласительных ссылок)
if isInSkipRange(i) {
continue
}
// Экранируем HTML-спецсимволы
switch runes[i] {
case '<':