diff --git a/go.mod b/go.mod index 6fe7af34..1eab4366 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,7 @@ require ( github.com/tinode/snowflake v1.0.0 go.mongodb.org/mongo-driver v1.12.1 golang.org/x/crypto v0.21.0 + golang.org/x/net v0.23.0 golang.org/x/oauth2 v0.16.0 golang.org/x/text v0.14.0 google.golang.org/api v0.148.0 @@ -68,7 +69,6 @@ require ( github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect go.opencensus.io v0.24.0 // indirect - golang.org/x/net v0.23.0 // indirect golang.org/x/sync v0.4.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/server/linkpreview.go b/server/linkpreview.go new file mode 100644 index 00000000..8c6d9545 --- /dev/null +++ b/server/linkpreview.go @@ -0,0 +1,113 @@ +package main + +import ( + "encoding/json" + "golang.org/x/net/html" + "net/http" + "strings" + "time" +) + +type LinkPreview struct { + Title string `json:"title"` + Description string `json:"description"` + Image string `json:"image"` + URL string `json:"url"` +} + +// PreviewLink handles the HTTP request, fetches the URL, and returns the link preview +func PreviewLink(w http.ResponseWriter, r *http.Request) { + url := r.URL.Query().Get("url") + if url == "" { + http.Error(w, "Missing 'url' query parameter", http.StatusBadRequest) + return + } + + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + client := &http.Client{ + Timeout: time.Second * 5, + } + resp, err := client.Do(req) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + http.Error(w, "Non-OK HTTP status", http.StatusInternalServerError) + return + } + + doc, err := html.Parse(resp.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + + metadata := extractMetadata(doc) + + linkPreview := LinkPreview{ + Title: metadata["og:title"], + Description: metadata["og:description"], + Image: metadata["og:image"], + URL: url, + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(linkPreview); err != nil { + http.Error(w, "Failed to encode response", http.StatusInternalServerError) + } +} + +func extractMetadata(n *html.Node) map[string]string { + metaTags := map[string]string{} + var traverse func(*html.Node) + traverse = func(n *html.Node) { + if n.Type == html.ElementNode && n.Data == "meta" { + attrs := make(map[string]string) + for _, attr := range n.Attr { + attrs[attr.Key] = attr.Val + } + name := attrs["name"] + property := attrs["property"] + content := attrs["content"] + + if strings.HasPrefix(property, "og:") && content != "" { + metaTags[property] = content + } else if name != "" && content != "" { + metaTags[name] = content + } + } + + for child := n.FirstChild; child != nil; child = child.NextSibling { + traverse(child) + } + } + + traverse(n) + + if _, exists := metaTags["og:title"]; !exists { + metaTags["og:title"] = extractTitle(n) + } + return metaTags +} + +func extractTitle(n *html.Node) string { + if n.Type == html.ElementNode && n.Data == "title" { + if n.FirstChild != nil { + return n.FirstChild.Data + } + } + for child := n.FirstChild; child != nil; child = child.NextSibling { + title := extractTitle(child) + if title != "" { + return title + } + } + return "" +} diff --git a/server/main.go b/server/main.go index a7b1c24e..906631a5 100644 --- a/server/main.go +++ b/server/main.go @@ -734,6 +734,8 @@ func main() { mux.HandleFunc("/", serve404) } + mux.HandleFunc(config.ApiPath+"v0/preview-link", PreviewLink) + if err = listenAndServe(config.Listen, mux, tlsConfig, signalHandler()); err != nil { logs.Err.Fatal(err) }