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