MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m52s
- GET /link-preview?url= fetches the page on the server (https only, SSRF-guarded like webhooks), reads Open Graph/Twitter tags, title, description and icon from the head, loads picture and icon (no SVG) and keeps them in link_previews (migration 6) for 7 days; readers' browsers never contact the page. Off in the demo; 300 fetches per address in 15 minutes. - Compact card above the bullet (icon, site, title, one line, small picture); the duplicate link badge is left out. - ✕ turns the address into [title](url), so hiding syncs with the note; "Link-Vorschauen anzeigen" under appearance switches all off per device. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
88 lines
3.2 KiB
Go
88 lines
3.2 KiB
Go
package storage
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"errors"
|
|
"time"
|
|
)
|
|
|
|
// Link previews: title, description, a small picture and the site icon of a web page, fetched once by
|
|
// the server and kept for a while, so readers' browsers never contact the page themselves.
|
|
|
|
type LinkPreview struct {
|
|
Key string `json:"key"`
|
|
URL string `json:"url"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description"`
|
|
SiteName string `json:"siteName"`
|
|
HasImage bool `json:"hasImage"`
|
|
HasIcon bool `json:"hasIcon"`
|
|
OK bool `json:"ok"` // false: the page gave nothing usable (kept shorter, then tried again)
|
|
FetchedAt string `json:"fetchedAt"`
|
|
|
|
Image, Icon []byte `json:"-"`
|
|
ImageMime, IconMime string `json:"-"`
|
|
}
|
|
|
|
// LinkPreviewKey identifies the preview of a URL (also used in the picture URLs).
|
|
func LinkPreviewKey(url string) string {
|
|
sum := sha256.Sum256([]byte(url))
|
|
return hex.EncodeToString(sum[:12])
|
|
}
|
|
|
|
func (s *Store) migrateLinkPreviews() error {
|
|
return s.execInTx(stmt(`CREATE TABLE IF NOT EXISTS link_previews (
|
|
key TEXT PRIMARY KEY,
|
|
url TEXT NOT NULL,
|
|
title TEXT NOT NULL DEFAULT '',
|
|
description TEXT NOT NULL DEFAULT '',
|
|
site_name TEXT NOT NULL DEFAULT '',
|
|
image BLOB,
|
|
image_mime TEXT NOT NULL DEFAULT '',
|
|
icon BLOB,
|
|
icon_mime TEXT NOT NULL DEFAULT '',
|
|
ok INTEGER NOT NULL DEFAULT 0,
|
|
fetched_at TEXT NOT NULL
|
|
)`))
|
|
}
|
|
|
|
var ErrLinkPreviewNotFound = errors.New("Keine Vorschau vorhanden")
|
|
|
|
func (s *Store) GetLinkPreview(key string) (*LinkPreview, error) {
|
|
var p LinkPreview
|
|
var ok int
|
|
err := s.db.QueryRow(`SELECT key, url, title, description, site_name, image, image_mime, icon, icon_mime, ok, fetched_at
|
|
FROM link_previews WHERE key = ?`, key).
|
|
Scan(&p.Key, &p.URL, &p.Title, &p.Description, &p.SiteName, &p.Image, &p.ImageMime, &p.Icon, &p.IconMime, &ok, &p.FetchedAt)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ErrLinkPreviewNotFound
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
p.OK = ok == 1
|
|
p.HasImage, p.HasIcon = len(p.Image) > 0, len(p.Icon) > 0
|
|
return &p, nil
|
|
}
|
|
|
|
func (s *Store) SaveLinkPreview(p *LinkPreview) error {
|
|
p.Key = LinkPreviewKey(p.URL)
|
|
p.FetchedAt = time.Now().UTC().Format(time.RFC3339)
|
|
_, err := s.db.Exec(`INSERT INTO link_previews (key, url, title, description, site_name, image, image_mime, icon, icon_mime, ok, fetched_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON CONFLICT(key) DO UPDATE SET url = excluded.url, title = excluded.title, description = excluded.description,
|
|
site_name = excluded.site_name, image = excluded.image, image_mime = excluded.image_mime, icon = excluded.icon,
|
|
icon_mime = excluded.icon_mime, ok = excluded.ok, fetched_at = excluded.fetched_at`,
|
|
p.Key, p.URL, p.Title, p.Description, p.SiteName, p.Image, p.ImageMime, p.Icon, p.IconMime, boolInt(p.OK), p.FetchedAt)
|
|
p.HasImage, p.HasIcon = len(p.Image) > 0, len(p.Icon) > 0
|
|
return err
|
|
}
|
|
|
|
// PruneLinkPreviews forgets previews older than the given time (they are fetched again when needed).
|
|
func (s *Store) PruneLinkPreviews(olderThan time.Time) error {
|
|
_, err := s.db.Exec(`DELETE FROM link_previews WHERE fetched_at < ?`, olderThan.UTC().Format(time.RFC3339))
|
|
return err
|
|
}
|