правки
This commit is contained in:
380
tg_bot/screens/channel_parser.go
Normal file
380
tg_bot/screens/channel_parser.go
Normal file
@@ -0,0 +1,380 @@
|
||||
package screens
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ChannelInput представляет результат парсинга ввода пользователя
|
||||
type ChannelInput struct {
|
||||
Input string // оригинальный ввод пользователя
|
||||
Username string // извлеченный username (если применимо)
|
||||
InviteLink string // извлеченный invite link (если применимо)
|
||||
Source string // источник: telegram, tgstat, telemetr
|
||||
Type string // тип: username, invite, deeplink, stat
|
||||
Valid bool // валидный ли формат
|
||||
}
|
||||
|
||||
// Парсеры для разных форматов
|
||||
var (
|
||||
// Username без @: 5-32 символа, начинается с буквы
|
||||
channelUsernameRe = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{4,31}$`)
|
||||
|
||||
// Invite link: t.me/+xxxxx или t.me/joinchat/xxxxx
|
||||
inviteLinkRe = regexp.MustCompile(`^t\.me/\+[a-zA-Z0-9_-]+$`)
|
||||
inviteLinkJoinRe = regexp.MustCompile(`^t\.me/joinchat/[a-zA-Z0-9_-]+$`)
|
||||
|
||||
// TGStat: tgstat.ru/channel_name или tgstat.ru/channel/@username
|
||||
tgstatRe = regexp.MustCompile(`^(?:https?://)?(?:www\.)?tgstat\.ru/(?:channel/)?@?([A-Za-z][A-Za-z0-9_.]{4,31})`)
|
||||
|
||||
// Telemetr: telemetr.io/channel/username или telemetr.io/channel/@username
|
||||
telemetrRe = regexp.MustCompile(`^(?:https?://)?(?:www\.)?telemetr\.io/(?:channel|channels)/@?([A-Za-z][A-Za-z0-9_.]{4,31})`)
|
||||
)
|
||||
|
||||
// ParseChannelInput парсит ввод пользователя и определяет тип канала
|
||||
func ParseChannelInput(input string) ChannelInput {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return ChannelInput{Valid: false}
|
||||
}
|
||||
|
||||
result := ChannelInput{Input: input}
|
||||
|
||||
// 1. Проверяем Telegram deeplink (tg://...)
|
||||
if strings.HasPrefix(input, "tg://") {
|
||||
return parseTelegramDeeplink(input)
|
||||
}
|
||||
|
||||
// 2. Проверяем invite ссылки (t.me/+xxx или t.me/joinchat/xxx)
|
||||
if inviteLinkRe.MatchString(input) || inviteLinkJoinRe.MatchString(input) {
|
||||
result.Type = "invite"
|
||||
result.Source = "telegram"
|
||||
result.InviteLink = normalizeInviteLink(input)
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
|
||||
// 3. Проверяем URL форматы (http://, https://, t.me/, telegram.me/)
|
||||
if isURLFormat(input) {
|
||||
return parseURLFormat(input)
|
||||
}
|
||||
|
||||
// 4. Проверяем @username
|
||||
if strings.HasPrefix(input, "@") {
|
||||
username := strings.TrimPrefix(input, "@")
|
||||
username = strings.TrimSpace(username)
|
||||
if channelUsernameRe.MatchString(username) {
|
||||
result.Username = username
|
||||
result.Type = "username"
|
||||
result.Source = "telegram"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
result.Valid = false
|
||||
return result
|
||||
}
|
||||
|
||||
// 5. Проверяем username (без @)
|
||||
if channelUsernameRe.MatchString(input) {
|
||||
result.Username = input
|
||||
result.Type = "username"
|
||||
result.Source = "telegram"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
|
||||
// 6. Ничего не подошло
|
||||
result.Valid = false
|
||||
return result
|
||||
}
|
||||
|
||||
// parseTelegramDeeplink парсит tg:// ссылки
|
||||
func parseTelegramDeeplink(input string) ChannelInput {
|
||||
result := ChannelInput{Input: input, Source: "telegram", Type: "deeplink"}
|
||||
|
||||
// tg://resolve?domain=username&post=123
|
||||
if strings.HasPrefix(input, "tg://resolve?domain=") {
|
||||
parsed, err := url.Parse(input)
|
||||
if err != nil {
|
||||
result.Valid = false
|
||||
return result
|
||||
}
|
||||
query := parsed.Query()
|
||||
domain := query.Get("domain")
|
||||
if domain != "" && channelUsernameRe.MatchString(domain) {
|
||||
result.Username = domain
|
||||
result.Type = "username"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// tg://join?invite=abcdef
|
||||
if strings.HasPrefix(input, "tg://join?invite=") {
|
||||
parsed, err := url.Parse(input)
|
||||
if err != nil {
|
||||
result.Valid = false
|
||||
return result
|
||||
}
|
||||
query := parsed.Query()
|
||||
invite := query.Get("invite")
|
||||
if invite != "" {
|
||||
result.InviteLink = "t.me/+" + invite
|
||||
result.Type = "invite"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
result.Valid = false
|
||||
return result
|
||||
}
|
||||
|
||||
// isURLFormat проверяет, является ли ввод URL или ссылкой
|
||||
func isURLFormat(input string) bool {
|
||||
return strings.HasPrefix(input, "http://") ||
|
||||
strings.HasPrefix(input, "https://") ||
|
||||
strings.HasPrefix(input, "t.me/") ||
|
||||
strings.HasPrefix(input, "telegram.me/") ||
|
||||
strings.Contains(input, "t.me/") ||
|
||||
strings.Contains(input, "telegram.me/")
|
||||
}
|
||||
|
||||
// parseURLFormat парсит URL-форматы ссылок
|
||||
func parseURLFormat(input string) ChannelInput {
|
||||
result := ChannelInput{Input: input}
|
||||
|
||||
// Нормализуем: убираем протокол
|
||||
normalized := input
|
||||
normalized = strings.TrimPrefix(normalized, "https://")
|
||||
normalized = strings.TrimPrefix(normalized, "http://")
|
||||
normalized = strings.TrimPrefix(normalized, "www.")
|
||||
|
||||
// Проверяем TGStat
|
||||
if matches := tgstatRe.FindStringSubmatch(input); len(matches) > 1 {
|
||||
username := strings.TrimSuffix(matches[1], "_")
|
||||
result.Username = username
|
||||
result.Type = "stat"
|
||||
result.Source = "tgstat"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
|
||||
// Проверяем Telemetr
|
||||
if matches := telemetrRe.FindStringSubmatch(input); len(matches) > 1 {
|
||||
username := strings.TrimSuffix(matches[1], "_")
|
||||
result.Username = username
|
||||
result.Type = "stat"
|
||||
result.Source = "telemetr"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
|
||||
// Проверяем telegram.me или t.me
|
||||
if strings.HasPrefix(normalized, "t.me/") || strings.HasPrefix(normalized, "telegram.me/") {
|
||||
return parseTelegramLink(normalized)
|
||||
}
|
||||
|
||||
result.Valid = false
|
||||
return result
|
||||
}
|
||||
|
||||
// parseTelegramLink парсит ссылки t.me/... и telegram.me/...
|
||||
func parseTelegramLink(link string) ChannelInput {
|
||||
result := ChannelInput{Source: "telegram"}
|
||||
|
||||
// Убираем t.me/ или telegram.me/
|
||||
path := link
|
||||
if strings.HasPrefix(path, "telegram.me/") {
|
||||
path = strings.TrimPrefix(path, "telegram.me/")
|
||||
} else {
|
||||
path = strings.TrimPrefix(path, "t.me/")
|
||||
}
|
||||
|
||||
// Проверяем invite link: +xxxxx или joinchat/xxxxx
|
||||
if strings.HasPrefix(path, "+") {
|
||||
inviteHash := strings.TrimPrefix(path, "+")
|
||||
inviteHash = extractBeforeChar(inviteHash, '?', '/')
|
||||
if inviteHash != "" {
|
||||
result.InviteLink = "t.me/+" + inviteHash
|
||||
result.Type = "invite"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "joinchat/") {
|
||||
inviteHash := strings.TrimPrefix(path, "joinchat/")
|
||||
inviteHash = extractBeforeChar(inviteHash, '?', '/')
|
||||
if inviteHash != "" {
|
||||
result.InviteLink = "t.me/joinchat/" + inviteHash
|
||||
result.Type = "invite"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Извлекаем username из t.me/username или t.me/username/12345
|
||||
parts := strings.SplitN(path, "/", 2)
|
||||
if len(parts) > 0 {
|
||||
username := parts[0]
|
||||
// Убираем @ если есть
|
||||
username = strings.TrimPrefix(username, "@")
|
||||
|
||||
// Валидируем username
|
||||
if channelUsernameRe.MatchString(username) {
|
||||
result.Username = username
|
||||
result.Type = "username"
|
||||
result.Valid = true
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
result.Valid = false
|
||||
return result
|
||||
}
|
||||
|
||||
// normalizeInviteLink нормализует invite ссылку
|
||||
func normalizeInviteLink(link string) string {
|
||||
link = strings.TrimSpace(link)
|
||||
|
||||
// Добавляем протокол если нужно
|
||||
if !strings.HasPrefix(link, "http://") && !strings.HasPrefix(link, "https://") && !strings.HasPrefix(link, "t.me/") {
|
||||
if strings.HasPrefix(link, "t.me/") {
|
||||
// Уже в нужном формате
|
||||
return link
|
||||
}
|
||||
}
|
||||
|
||||
// Нормализуем t.me/+xxxxx
|
||||
if strings.HasPrefix(link, "t.me/+") {
|
||||
return link
|
||||
}
|
||||
|
||||
// Нормализуем t.me/joinchat/xxxxx
|
||||
if strings.HasPrefix(link, "t.me/joinchat/") {
|
||||
return link
|
||||
}
|
||||
|
||||
return link
|
||||
}
|
||||
|
||||
// extractBeforeChar извлекает часть строки до первого встреченного символа
|
||||
func extractBeforeChar(s string, chars ...rune) string {
|
||||
for _, c := range chars {
|
||||
if idx := strings.IndexRune(s, c); idx != -1 {
|
||||
return s[:idx]
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SuggestFix предлагает исправление для некорректного ввода
|
||||
func SuggestFix(input string) (string, bool) {
|
||||
input = strings.TrimSpace(input)
|
||||
if input == "" {
|
||||
return "", false
|
||||
}
|
||||
|
||||
// 1. Исправляем опечатки в протоколе
|
||||
if strings.HasPrefix(input, "htp://") {
|
||||
fixed := strings.Replace(input, "htp://", "https://", 1)
|
||||
if parsed := ParseChannelInput(fixed); parsed.Valid {
|
||||
return fixed, true
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(input, "ttps://") {
|
||||
fixed := strings.Replace(input, "ttps://", "https://", 1)
|
||||
if parsed := ParseChannelInput(fixed); parsed.Valid {
|
||||
return fixed, true
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(input, "ttp://") {
|
||||
fixed := strings.Replace(input, "ttp://", "http://", 1)
|
||||
if parsed := ParseChannelInput(fixed); parsed.Valid {
|
||||
return fixed, true
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Исправляем tme/ → t.me/
|
||||
if strings.HasPrefix(input, "tme/") {
|
||||
fixed := strings.Replace(input, "tme/", "t.me/", 1)
|
||||
if parsed := ParseChannelInput(fixed); parsed.Valid {
|
||||
return fixed, true
|
||||
}
|
||||
}
|
||||
if strings.Contains(input, "tme/") {
|
||||
fixed := strings.Replace(input, "tme/", "t.me/", 1)
|
||||
if parsed := ParseChannelInput(fixed); parsed.Valid {
|
||||
return fixed, true
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Исправляем @@ или @@
|
||||
if strings.HasPrefix(input, "@@") {
|
||||
fixed := strings.TrimPrefix(input, "@")
|
||||
if parsed := ParseChannelInput(fixed); parsed.Valid {
|
||||
return fixed, true
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Пробуем добавить @ в начало username
|
||||
if !strings.HasPrefix(input, "@") && !isURLFormat(input) {
|
||||
fixed := "@" + input
|
||||
if parsed := ParseChannelInput(fixed); parsed.Valid {
|
||||
return fixed, true
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Пробуем убрать лишние символы в конце
|
||||
cleaned := input
|
||||
cleaned = strings.TrimSuffix(cleaned, ".")
|
||||
cleaned = strings.TrimSuffix(cleaned, ",")
|
||||
cleaned = strings.TrimSuffix(cleaned, ";")
|
||||
cleaned = strings.TrimSpace(cleaned)
|
||||
if cleaned != input {
|
||||
if parsed := ParseChannelInput(cleaned); parsed.Valid {
|
||||
return cleaned, true
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Пробуем извлечь username из сложной ссылки
|
||||
if strings.Contains(input, "/") {
|
||||
parts := strings.Split(input, "/")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
part := strings.TrimSpace(parts[i])
|
||||
part = strings.TrimPrefix(part, "@")
|
||||
if channelUsernameRe.MatchString(part) {
|
||||
return "@" + part, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", false
|
||||
}
|
||||
|
||||
// FormatChannelLabel форматирует метку канала для отображения
|
||||
func FormatChannelLabel(input ChannelInput) string {
|
||||
if input.Username != "" {
|
||||
return "@" + input.Username
|
||||
}
|
||||
if input.InviteLink != "" {
|
||||
return formatInviteLabel(input.InviteLink)
|
||||
}
|
||||
return input.Input
|
||||
}
|
||||
|
||||
// IsDuplicate проверяет, является ли канал дубликатом (case-insensitive для username)
|
||||
func IsDuplicate(input ChannelInput, channels []PurchaseChannelInput) bool {
|
||||
for _, ch := range channels {
|
||||
// Case-insensitive сравнение username (@MyChannel == @mychannel)
|
||||
if input.Username != "" && ch.Username != "" && strings.EqualFold(ch.Username, input.Username) {
|
||||
return true
|
||||
}
|
||||
if input.InviteLink != "" && ch.InviteLink == input.InviteLink {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user