butterrobot/internal/urlutil/urlutil.go
Felipe M. 8801c43f92
All checks were successful
CI / goreleaser-lint (push) Successful in 10s
CI / format (push) Successful in 1m52s
Release / release (push) Successful in 5m40s
CI / lint (push) Successful in 2m31s
CI / test (push) Successful in 1m0s
CI / build (push) Successful in 4m6s
feat: add !gallerydl command to download media from links
Reply to a message containing a link with !gallerydl (or send it with a
link inline) to download the media via the gallery-dl CLI and have the
files posted back as a reply. Restricted to an allowed_users allowlist
(comma-separated, per-channel/global config); no one is allowed by
default.

- Add media support to the platform layer: model.Message.Files,
  ActionSendMedia, and Platform.SendMedia. Telegram uploads via
  multipart (sendPhoto/sendVideo/sendDocument, 50MB cap); Slack returns
  unsupported for now.
- Add internal/shell (os/exec wrapper) and internal/urlutil (URL
  extraction) helpers with tests.
- Dispatch ActionSendMedia in app and clean up the temp download dir.
- Admin config UI for allowed_users / gallery_dl_path; docs entry.
- Containerfile: base on alpine and install gallery-dl + ffmpeg (was
  scratch); release workflow sets up QEMU + Buildx for cross-arch image
  builds.
2026-07-05 20:05:57 +02:00

53 lines
1.3 KiB
Go

// Package urlutil provides helpers for pulling URLs out of arbitrary text.
package urlutil
import (
"net/url"
"regexp"
)
// urlPattern matches http(s) URLs embedded in free-form text.
var urlPattern = regexp.MustCompile(`https?://[^\s]+`)
// ExtractURLs returns the http(s) URLs found in text, in order of appearance
// and without duplicates. Trailing punctuation that is unlikely to be part of
// a URL (e.g. a sentence-ending period or a wrapping parenthesis) is trimmed,
// and each candidate is validated with url.Parse.
func ExtractURLs(text string) []string {
matches := urlPattern.FindAllString(text, -1)
seen := make(map[string]struct{}, len(matches))
urls := make([]string, 0, len(matches))
for _, match := range matches {
candidate := trimURL(match)
parsed, err := url.Parse(candidate)
if err != nil || parsed.Host == "" {
continue
}
if _, ok := seen[candidate]; ok {
continue
}
seen[candidate] = struct{}{}
urls = append(urls, candidate)
}
return urls
}
// trimURL strips common trailing characters that are almost never part of the
// URL itself when it appears inside a sentence.
func trimURL(s string) string {
for len(s) > 0 {
last := s[len(s)-1]
switch last {
case '.', ',', ';', ':', '!', '?', ')', ']', '}', '"', '\'', '>':
s = s[:len(s)-1]
default:
return s
}
}
return s
}