sync: phase 1 backend + frontend from server
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
package embed
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var urlRegex = regexp.MustCompile(`https?://[^\s<>"{}|\^\[\]]+`)
|
||||
|
||||
// Embed represents a stored link preview.
|
||||
type Embed struct {
|
||||
ID string `json:"id"`
|
||||
MessageID string `json:"message_id"`
|
||||
URL string `json:"url"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
ImageURL string `json:"image_url"`
|
||||
SiteName string `json:"site_name"`
|
||||
}
|
||||
|
||||
// ExtractURLs returns all HTTP/HTTPS URLs found in text.
|
||||
func ExtractURLs(text string) []string {
|
||||
matches := urlRegex.FindAllString(text, -1)
|
||||
seen := make(map[string]struct{})
|
||||
var out []string
|
||||
for _, u := range matches {
|
||||
if _, ok := seen[u]; ok {
|
||||
continue
|
||||
}
|
||||
seen[u] = struct{}{}
|
||||
out = append(out, u)
|
||||
}
|
||||
if len(out) > 5 {
|
||||
out = out[:5]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// FetchEmbed retrieves OpenGraph meta tags for a URL.
|
||||
func FetchEmbed(url string) *Embed {
|
||||
client := &http.Client{Timeout: 2 * time.Second}
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; DumpsterBot/1.0)")
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil
|
||||
}
|
||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
html := string(body)
|
||||
return &Embed{
|
||||
URL: url,
|
||||
Title: ogTag(html, "og:title"),
|
||||
Description: ogTag(html, "og:description"),
|
||||
ImageURL: ogTag(html, "og:image"),
|
||||
SiteName: ogTag(html, "og:site_name"),
|
||||
}
|
||||
}
|
||||
|
||||
func ogTag(html, property string) string {
|
||||
// Try property
|
||||
re := regexp.MustCompile(`<meta[^>]+property=["']` + regexp.QuoteMeta(property) + `["'][^>]+content=["']([^"']*)["']`)
|
||||
m := re.FindStringSubmatch(html)
|
||||
if len(m) > 1 && m[1] != "" {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
// Try name
|
||||
re = regexp.MustCompile(`<meta[^>]+name=["']` + regexp.QuoteMeta(property) + `["'][^>]+content=["']([^"']*)["']`)
|
||||
m = re.FindStringSubmatch(html)
|
||||
if len(m) > 1 && m[1] != "" {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
// Fallback for title
|
||||
if property == "og:title" {
|
||||
re = regexp.MustCompile(`<title[^>]*>([^<]+)</title>`)
|
||||
m = re.FindStringSubmatch(html)
|
||||
if len(m) > 1 {
|
||||
return strings.TrimSpace(m[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// FetchAndStore extracts URLs from content, fetches embeds, and stores them.
|
||||
func FetchAndStore(db *sql.DB, messageID string, content string) {
|
||||
urls := ExtractURLs(content)
|
||||
if len(urls) == 0 {
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
for _, url := range urls {
|
||||
emb := FetchEmbed(url)
|
||||
if emb == nil || (emb.Title == "" && emb.Description == "" && emb.ImageURL == "") {
|
||||
continue
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `
|
||||
INSERT INTO embeds (message_id, url, title, description, image_url, site_name)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`, messageID, emb.URL, emb.Title, emb.Description, emb.ImageURL, emb.SiteName)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to store embed: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LoadForMessages returns embeds grouped by message_id.
|
||||
func LoadForMessages(ctx context.Context, db *sql.DB, messageIDs []string) (map[string][]Embed, error) {
|
||||
result := make(map[string][]Embed)
|
||||
if len(messageIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
placeholders := make([]string, len(messageIDs))
|
||||
args := make([]interface{}, len(messageIDs))
|
||||
for i, id := range messageIDs {
|
||||
placeholders[i] = fmt.Sprintf("$%d", i+1)
|
||||
args[i] = id
|
||||
}
|
||||
query := "SELECT message_id, id, url, title, description, image_url, site_name FROM embeds WHERE message_id IN (" + strings.Join(placeholders, ",") + ")"
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var e Embed
|
||||
var mid string
|
||||
if err := rows.Scan(&mid, &e.ID, &e.URL, &e.Title, &e.Description, &e.ImageURL, &e.SiteName); err != nil {
|
||||
continue
|
||||
}
|
||||
result[mid] = append(result[mid], e)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user