internal/domain/webpage.go
Rama main · réplica de lectura del repositorio original.
package domain
import (
"hash/fnv"
"net/url"
"strings"
)
// NormalizeWebPageURL normalizes a link in a user message into a stable form for dedup: it accepts only http/https,
// rejects URLs with userinfo (SSRF/impersonation defense), lowercases the scheme and host, drops the default port and fragment,
// and keeps the path and query as-is. It returns the normalized URL and whether it is previewable (false when not previewable).
func NormalizeWebPageURL(raw string) (string, bool) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", false
}
u, err := url.Parse(raw)
if err != nil {
return "", false
}
scheme := strings.ToLower(u.Scheme)
if scheme != "http" && scheme != "https" {
return "", false
}
if u.User != nil {
return "", false
}
host := strings.ToLower(u.Host)
if host == "" {
return "", false
}
// Drop the default port matching the scheme, so example.com and example.com:80 do not hash differently.
if h, port, ok := splitHostPort(host); ok {
if (scheme == "http" && port == "80") || (scheme == "https" && port == "443") {
host = h
}
}
out := scheme + "://" + host + u.EscapedPath()
if u.RawQuery != "" {
out += "?" + u.RawQuery
}
return out, true
}
// splitHostPort splits host:port; ok=false when there is no port. It does not use net.SplitHostPort so that a bare IPv6
// literal host (no port) is not treated as an error.
func splitHostPort(host string) (string, string, bool) {
// An IPv6 literal looks like [::1]:443; the port is only searched for after the trailing ].
idx := strings.LastIndexByte(host, ':')
if idx < 0 {
return host, "", false
}
if close := strings.LastIndexByte(host, ']'); close >= 0 && idx < close {
return host, "", false
}
return host[:idx], host[idx+1:], true
}
// IsPendingWebPageMedia reports whether media is a pending link preview placeholder with ID==id (the
// idempotency guard for async parsing: only this state may be replaced in place by a resolved result).
func IsPendingWebPageMedia(m *MessageMedia, id int64) bool {
return m != nil &&
m.Kind == MessageMediaKindWebPage &&
m.WebPage != nil &&
m.WebPage.State == MessageWebPageStatePending &&
m.WebPage.ID == id
}
// WebPageURLHash computes a stable 63-bit hash of the normalized URL, used both as the web_pages row primary key and
// as the webPage id (so a pending placeholder and its done resolution carry the same id). FNV-1a/64, made positive.
func WebPageURLHash(normalized string) int64 {
h := fnv.New64a()
_, _ = h.Write([]byte(normalized))
return int64(h.Sum64() & 0x7fffffffffffffff)
}