package telegram import ( "fmt" "html" "net/url" "regexp" "strings" "olexsmir.xyz/rss-tools/app" ) var ( linkRe = regexp.MustCompile(`https?://[^\s<>"']+`) youtubeIDRe = regexp.MustCompile(`^[A-Za-z0-9_-]{11}$`) trailingPunctRe = regexp.MustCompile(`[.,!?:;)]+$`) ) type foundLink struct { start int end int raw string } func findLinks(text string) []foundLink { indexes := linkRe.FindAllStringIndex(text, -1) links := make([]foundLink, 0, len(indexes)) for _, idx := range indexes { start, end := idx[0], idx[1] candidate := text[start:end] trimmed := trailingPunctRe.ReplaceAllString(candidate, "") if trimmed == "" { continue } trimmedEnd := start + len(trimmed) if !isHTTPURL(trimmed) { continue } links = append(links, foundLink{ start: start, end: trimmedEnd, raw: trimmed, }) } return links } func isHTTPURL(raw string) bool { u, err := url.Parse(raw) if err != nil || u.Host == "" { return false } return u.Scheme == "http" || u.Scheme == "https" } func linkifyMessageText(text string) (string, []string) { links := findLinks(text) if len(links) == 0 { return html.EscapeString(text), nil } var b strings.Builder urls := make([]string, 0, len(links)) last := 0 for _, l := range links { if l.start < last { continue } b.WriteString(html.EscapeString(text[last:l.start])) escaped := html.EscapeString(l.raw) fmt.Fprintf(&b, `%s`, escaped, escaped) urls = append(urls, l.raw) last = l.end } b.WriteString(html.EscapeString(text[last:])) return b.String(), urls } func messageLinks(text string) []string { links := findLinks(text) out := make([]string, 0, len(links)) seen := make(map[string]struct{}, len(links)) for _, link := range links { if _, ok := seen[link.raw]; ok { continue } seen[link.raw] = struct{}{} out = append(out, link.raw) } return out } func feedLinks(urls []string) []app.FeedLink { links := make([]app.FeedLink, 0, len(urls)) for _, u := range urls { links = append(links, app.FeedLink{ Rel: "alternate", Type: "text/html", Href: u, }) } return links } func youtubeCanonicalLink(raw string) (string, string, bool) { u, err := url.Parse(raw) if err != nil { return "", "", false } host := strings.ToLower(u.Hostname()) host = strings.TrimPrefix(host, "www.") host = strings.TrimPrefix(host, "m.") videoID := "" switch host { case "youtube.com", "youtube-nocookie.com": path := strings.TrimSuffix(u.Path, "/") switch path { case "/watch": videoID = u.Query().Get("v") default: if afterShort, okShort := strings.CutPrefix(path, "/shorts/"); okShort { videoID = afterShort } else if afterLive, okLive := strings.CutPrefix(path, "/live/"); okLive { videoID = afterLive } } case "youtu.be": videoID = strings.Trim(u.Path, "/") default: return "", "", false } if !youtubeIDRe.MatchString(videoID) { return "", "", false } canonical := "https://www.youtube.com/watch?v=" + videoID return canonical, videoID, true } func normalizeLinks(rawLinks []string) []string { out := make([]string, 0, len(rawLinks)) seen := make(map[string]struct{}, len(rawLinks)) for _, raw := range rawLinks { normalized := raw if canonical, _, ok := youtubeCanonicalLink(raw); ok { normalized = canonical } if _, ok := seen[normalized]; ok { continue } seen[normalized] = struct{}{} out = append(out, normalized) } return out } func firstYouTubeVideoID(urls []string) (string, bool) { for _, u := range urls { _, videoID, ok := youtubeCanonicalLink(u) if ok { return videoID, true } } return "", false }