Ctrl K
ComunidadGuías…

internal/domain/webpage.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
}