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 без @: 4-32 символа, начинается с буквы channelUsernameRe = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_]{3,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_.]{3,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_.]{3,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 }